Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8215a9640 | ||
|
|
0e6cae9875 | ||
|
|
888bac9408 | ||
|
|
23d86deae7 | ||
|
|
7d9c0b0298 | ||
|
|
949ffa01ad | ||
|
|
a33e51351b | ||
|
|
a6706fcb0c | ||
|
|
d06cbcaa91 | ||
|
|
cd2b82d614 | ||
|
|
eada9e43ec | ||
|
|
d1cb3f0b30 | ||
|
|
857b2b5c53 | ||
|
|
d74c3f4598 | ||
|
|
f1cb8ce5b3 | ||
|
|
8f32082145 | ||
|
|
c1b7911518 | ||
|
|
acbebd67a3 | ||
|
|
4c17c520d7 | ||
|
|
1468df3e21 | ||
|
|
7e60eb40b4 |
@@ -10,8 +10,12 @@
|
||||
"Bash(npm test)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(sed:*)"
|
||||
],
|
||||
"deny": []
|
||||
"deny": [],
|
||||
"defaultMode": "acceptEdits"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ labels:
|
||||
- label: 'API'
|
||||
title: '.*openapi.*|.*swagger.*|.*api.*'
|
||||
|
||||
- label: 'v2'
|
||||
base-branch: 'V2'
|
||||
|
||||
- label: 'Translation'
|
||||
files:
|
||||
- 'app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
|
||||
@@ -62,6 +65,7 @@ labels:
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
- 'frontend/**'
|
||||
|
||||
- label: 'Java'
|
||||
files:
|
||||
@@ -120,6 +124,7 @@ labels:
|
||||
- 'scripts/installFonts.sh'
|
||||
- 'test.sh'
|
||||
- 'test2.sh'
|
||||
- 'docker/**'
|
||||
|
||||
- label: 'Devtools'
|
||||
files:
|
||||
@@ -131,7 +136,6 @@ labels:
|
||||
- '.github/workflows/pre_commit.yml'
|
||||
- 'devGuide/.*'
|
||||
- 'devTools/.*'
|
||||
- 'devTools/.*'
|
||||
|
||||
- label: 'Test'
|
||||
files:
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
color: "DEDEDE"
|
||||
- name: "v2"
|
||||
color: "FFFF00"
|
||||
description: "Issues or pull requests related to the v2 branch"
|
||||
- name: "wontfix"
|
||||
description: "This will not be worked on"
|
||||
color: "FFFFFF"
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- name: AI PR Title Analysis
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: ai-title-analysis
|
||||
uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7
|
||||
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt-file: ".github/config/system-prompt.txt"
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
+17
-12
@@ -31,8 +31,7 @@ jobs:
|
||||
project: ${{ steps.changes.outputs.project }}
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@v4.3.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@v3.0.2
|
||||
@@ -56,14 +55,15 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4.3.0
|
||||
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4.4.1
|
||||
uses: gradle/actions/setup-gradle@v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
@@ -106,19 +106,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@v2.12.2
|
||||
uses: step-security/harden-runner@v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
- uses: gradle/actions/setup-gradle@v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@v4.4.2
|
||||
- name: Generate OpenAPI documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
@@ -163,7 +168,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4.3.0
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
@@ -205,7 +210,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -254,7 +259,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -263,7 +268,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
@@ -22,6 +22,6 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Run Labeler
|
||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: stirling-${{ matrix.file_suffix }}binaries
|
||||
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: ${{ matrix.platform }}binaries
|
||||
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download signed artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: binaries${{ matrix.file_suffix }}
|
||||
- name: Display structure of downloaded files
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download signed artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: signed${{ matrix.file_suffix }}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -74,6 +74,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -34,12 +34,12 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Build and analyze with Gradle
|
||||
env:
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -48,6 +49,14 @@ public class AppConfig {
|
||||
@Value("${server.port:8080}")
|
||||
private String serverPort;
|
||||
|
||||
@Value("${v2}")
|
||||
public boolean v2Enabled;
|
||||
|
||||
@Bean
|
||||
public boolean v2Enabled() {
|
||||
return v2Enabled;
|
||||
}
|
||||
|
||||
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
@@ -119,7 +128,7 @@ public class AppConfig {
|
||||
public boolean rateLimit() {
|
||||
String rateLimit = System.getProperty("rateLimit");
|
||||
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
||||
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
|
||||
return Boolean.parseBoolean(rateLimit);
|
||||
}
|
||||
|
||||
@Bean(name = "RunningInDocker")
|
||||
@@ -139,8 +148,8 @@ public class AppConfig {
|
||||
if (!Files.exists(mountInfo)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
|
||||
try (Stream<String> lines = Files.lines(mountInfo)) {
|
||||
return lines.anyMatch(line -> line.contains(" /configs "));
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+6
@@ -25,6 +25,7 @@ public class InstallationPathConfig {
|
||||
private static final String STATIC_PATH;
|
||||
private static final String TEMPLATES_PATH;
|
||||
private static final String SIGNATURES_PATH;
|
||||
private static final String PRIVATE_KEY_PATH;
|
||||
|
||||
static {
|
||||
BASE_PATH = initializeBasePath();
|
||||
@@ -45,6 +46,7 @@ public class InstallationPathConfig {
|
||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
||||
PRIVATE_KEY_PATH = CONFIG_PATH + "db" + File.separator + "keys" + File.separator;
|
||||
}
|
||||
|
||||
private static String initializeBasePath() {
|
||||
@@ -120,4 +122,8 @@ public class InstallationPathConfig {
|
||||
public static String getSignaturesPath() {
|
||||
return SIGNATURES_PATH;
|
||||
}
|
||||
|
||||
public static String getPrivateKeyPath() {
|
||||
return PRIVATE_KEY_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ public class ApplicationProperties {
|
||||
private long loginResetTimeMinutes;
|
||||
private String loginMethod = "all";
|
||||
private String customGlobalAPIKey;
|
||||
private Jwt jwt = new Jwt();
|
||||
|
||||
public Boolean isAltLogin() {
|
||||
return saml2.getEnabled() || oauth2.getEnabled();
|
||||
@@ -298,6 +299,15 @@ public class ApplicationProperties {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Jwt {
|
||||
private boolean enableKeystore = true;
|
||||
private boolean enableKeyRotation = false;
|
||||
private boolean enableKeyCleanup = true;
|
||||
private int keyRetentionDays = 7;
|
||||
private boolean secureCookie;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -362,7 +372,8 @@ public class ApplicationProperties {
|
||||
public String getBaseTmpDir() {
|
||||
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
||||
? baseTmpDir
|
||||
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
|
||||
: java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
|
||||
+ "/stirling-pdf";
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
|
||||
@@ -14,8 +14,10 @@ public class RequestUriUtils {
|
||||
|| requestURI.startsWith(contextPath + "/images/")
|
||||
|| requestURI.startsWith(contextPath + "/public/")
|
||||
|| requestURI.startsWith(contextPath + "/pdfjs/")
|
||||
|| requestURI.startsWith(contextPath + "/pdfjs-legacy/")
|
||||
|| requestURI.startsWith(contextPath + "/login")
|
||||
|| requestURI.startsWith(contextPath + "/error")
|
||||
|| requestURI.startsWith(contextPath + "/favicon")
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|
||||
-1
@@ -37,7 +37,6 @@ public class ConvertHtmlToPDF {
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||
|
||||
@Operation(
|
||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||
description =
|
||||
|
||||
-1
@@ -46,7 +46,6 @@ public class ConvertMarkdownToPdf {
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||
|
||||
@Operation(
|
||||
summary = "Convert a Markdown file to PDF",
|
||||
description =
|
||||
|
||||
@@ -3,9 +3,9 @@ logging.level.org.springframework=WARN
|
||||
logging.level.org.hibernate=WARN
|
||||
logging.level.org.eclipse.jetty=WARN
|
||||
#logging.level.org.springframework.security.saml2=TRACE
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
#logging.level.org.springframework.security=DEBUG
|
||||
#logging.level.org.opensaml=DEBUG
|
||||
logging.level.stirling.software: DEBUG
|
||||
#logging.level.stirling.software.proprietary.security=DEBUG
|
||||
logging.level.com.zaxxer.hikari=WARN
|
||||
spring.jpa.open-in-view=false
|
||||
server.forward-headers-strategy=NATIVE
|
||||
@@ -57,12 +57,5 @@ spring.main.allow-bean-definition-overriding=true
|
||||
# Set up a consistent temporary directory location
|
||||
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
|
||||
|
||||
|
||||
#spring.security.oauth2.resourceserver.jwt.issuer-uri=https://nrlkjfznsavsbmweiyqu.supabase.co/auth/v1
|
||||
|
||||
spring.security.oauth2.resourceserver.jwk-set-uri=https://nrlkjfznsavsbmweiyqu.supabase.co/auth/v1/.well-known/jwks.json
|
||||
spring.security.oauth2.resourceserver.audience=authenticated
|
||||
logging:
|
||||
level:
|
||||
org.springframework.security: DEBUG
|
||||
your.project.pkg.security: DEBUG
|
||||
# V2 features
|
||||
v2=false
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=لقد تسجل دخولًا إلى
|
||||
login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى.
|
||||
login.toManySessions=لديك عدة جلسات نشطة
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=حجب تلقائي
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Siz artıq daxil olmusunuz
|
||||
login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin.
|
||||
login.toManySessions=Həddindən artıq aktiv sessiyanız var
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Avtomatik Gizlətmə
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Вече сте влезли в
|
||||
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
|
||||
login.toManySessions=Имате твърде много активни сесии
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Автоматично редактиране
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=ཁྱེད་རང་
|
||||
login.alreadyLoggedIn2=སྒྲིག་ཆས་ནང་ནང་འཛུལ་བྱས་ཟིན། སྒྲིག་ཆས་ནས་ཕྱིར་འཐེན་བྱས་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས།
|
||||
login.toManySessions=ཁྱེད་ལ་འཛུལ་ཞུགས་བྱས་པའི་གནས་སྐབས་མང་དྲགས་འདུག
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=རང་འགུལ་སྒྲིབ་སྲུང་།
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Ja has iniciat sessió a
|
||||
login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar.
|
||||
login.toManySessions=Tens massa sessions actives
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Redacció Automàtica
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Již jste přihlášeni na
|
||||
login.alreadyLoggedIn2=zařízeních. Odhlaste se prosím z těchto zařízení a zkuste to znovu.
|
||||
login.toManySessions=Máte příliš mnoho aktivních relací
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatické začernění
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Du er allerede logget ind på
|
||||
login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen.
|
||||
login.toManySessions=Du har for mange aktive sessoner
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Auto Rediger
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Sie sind bereits an
|
||||
login.alreadyLoggedIn2=Geräten angemeldet. Bitte melden Sie sich dort ab und versuchen es dann erneut.
|
||||
login.toManySessions=Sie haben zu viele aktive Sitzungen
|
||||
login.logoutMessage=Sie wurden erfolgreich abgemeldet.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatisch zensieren/schwärzen
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Είστε ήδη συνδεδεμένοι σε
|
||||
login.alreadyLoggedIn2=συσκευές. Παρακαλώ αποσυνδεθείτε από τις συσκευές και προσπαθήστε ξανά.
|
||||
login.toManySessions=Έχετε πάρα πολλές ενεργές συνεδρίες
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Αυτόματη απόκρυψη
|
||||
|
||||
@@ -893,7 +893,7 @@ login.rememberme=Remember me
|
||||
login.invalid=Invalid username or password.
|
||||
login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=Login via Single Sign-on
|
||||
login.ssoSignIn=Login via Single Sign-On
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
|
||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||
login.toManySessions=You have too many active sessions
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Auto Redact
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
|
||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||
login.toManySessions=You have too many active sessions
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Auto Redact
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Ya ha iniciado sesión en
|
||||
login.alreadyLoggedIn2=dispositivos. Cierre sesión en los dispositivos y vuelva a intentarlo.
|
||||
login.toManySessions=Tiene demasiadas sesiones activas
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Auto Censurar Texto
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
|
||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||
login.toManySessions=You have too many active sessions
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Auto Idatzi
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=شما قبلاً وارد شدهاید در
|
||||
login.alreadyLoggedIn2=دستگاهها. لطفاً از دستگاهها خارج شده و دوباره تلاش کنید.
|
||||
login.toManySessions=شما تعداد زیادی نشست فعال دارید.
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=سانسور خودکار
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Vous êtes déjà connecté sur
|
||||
login.alreadyLoggedIn2=appareils. Veuillez vous déconnecter des appareils et réessayer.
|
||||
login.toManySessions=Vous avez trop de sessions actives.
|
||||
login.logoutMessage=Vous avez été déconnecté.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Caviarder automatiquement
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Tá tú logáilte isteach cheana
|
||||
login.alreadyLoggedIn2=gléasanna. Logáil amach as na gléasanna agus bain triail eile as.
|
||||
login.toManySessions=Tá an iomarca seisiún gníomhach agat
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Auto Redact
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=आप पहले से ही
|
||||
login.alreadyLoggedIn2=उपकरणों में लॉग इन हैं। कृपया उपकरणों से लॉग आउट करें और पुनः प्रयास करें।
|
||||
login.toManySessions=आपके बहुत सारे सक्रिय सत्र हैं
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=स्वतः गोपनीयकरण
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Već ste se prijavili na
|
||||
login.alreadyLoggedIn2=ure. Odjavite se s ure i pokušajte ponovo.
|
||||
login.toManySessions=Imate preko mrežne sesije aktivnih
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatsko uređivanje
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Már be van jelentkezve
|
||||
login.alreadyLoggedIn2=eszközön. Kérjük, jelentkezzen ki az eszközökről és próbálja újra.
|
||||
login.toManySessions=Túl sok aktív munkamenet
|
||||
login.logoutMessage=Sikeresen kijelentkezett.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatikus kitakarás
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Anda sudah login ke
|
||||
login.alreadyLoggedIn2=perangkat. Silakan keluar dari perangkat dan coba lagi.
|
||||
login.toManySessions=Anda memiliki terlalu banyak sesi aktif
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Redaksional Otomatis
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Hai già effettuato l'accesso a
|
||||
login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova.
|
||||
login.toManySessions=Hai troppe sessioni attive
|
||||
login.logoutMessage=Sei stato disconnesso.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Redazione automatica
|
||||
@@ -1773,7 +1774,7 @@ audit.dashboard.filter.userPlaceholder=Filtra per utente
|
||||
audit.dashboard.filter.startDate=Data di inizio
|
||||
audit.dashboard.filter.endDate=Data di fine
|
||||
audit.dashboard.filter.apply=Applica filtri
|
||||
audit.dashboard.filter.reset=Resetta Filtri
|
||||
audit.dashboard.filter.reset=Resetta filtri
|
||||
|
||||
# Table Headers
|
||||
audit.dashboard.table.id=ID
|
||||
@@ -1863,7 +1864,7 @@ scannerEffect.submit=Crea una falsa scansione
|
||||
#home.scannerEffect
|
||||
home.scannerEffect.title=Falsa scansione
|
||||
home.scannerEffect.desc=Crea un PDF che sembra scansionato
|
||||
scannerEffect.tags=scansiona, simula, realistico, converti
|
||||
scannerEffect.tags=scansiona,simula,realistico,converti
|
||||
|
||||
# ScannerEffect advanced settings (frontend)
|
||||
scannerEffect.advancedSettings=Abilita impostazioni di scansione avanzate
|
||||
@@ -1885,7 +1886,7 @@ scannerEffect.resolution=Risoluzione (DPI)
|
||||
home.editTableOfContents.title=Modifica indice
|
||||
home.editTableOfContents.desc=Aggiungi o modifica segnalibri e sommario nei documenti PDF
|
||||
|
||||
editTableOfContents.tags=segnalibri, indice, navigazione, indice analitico, sommario, capitoli, sezioni, struttura
|
||||
editTableOfContents.tags=segnalibri,indice,navigazione,indice analitico,sommario,capitoli,sezioni,struttura
|
||||
editTableOfContents.title=Modifica indice
|
||||
editTableOfContents.header=Aggiungi o modifica sommario PDF
|
||||
editTableOfContents.replaceExisting=Sostituisci i segnalibri esistenti (deseleziona per aggiungerli a quelli esistenti)
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=すでにログインしています
|
||||
login.alreadyLoggedIn2=デバイスからログアウトしてもう一度お試しください。
|
||||
login.toManySessions=アクティブなセッションが多すぎます
|
||||
login.logoutMessage=ログアウトしました
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=自動墨消し
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=이미 다음에 로그인되어 있습니다
|
||||
login.alreadyLoggedIn2=개의 기기. 해당 기기에서 로그아웃한 후 다시 시도하세요.
|
||||
login.toManySessions=활성 세션이 너무 많습니다
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=자동 검열
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=നിങ്ങൾ ഇതിനകം ലോഗിൻ ച
|
||||
login.alreadyLoggedIn2=ഉപകരണങ്ങളിൽ. ദയവായി ഉപകരണങ്ങളിൽ നിന്ന് ലോഗ് ഔട്ട് ചെയ്ത് വീണ്ടും ശ്രമിക്കുക.
|
||||
login.toManySessions=നിങ്ങൾക്ക് വളരെയധികം സജീവ സെഷനുകൾ ഉണ്ട്
|
||||
login.logoutMessage=നിങ്ങൾ ലോഗ് ഔട്ട് ചെയ്തു.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=സ്വയം റെഡാക്റ്റ് ചെയ്യുക
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=U zit reeds ingelogd bij
|
||||
login.alreadyLoggedIn2=apparaten. U moet u a.u.b. uitloggen van de apparaten en opnieuw proberen.
|
||||
login.toManySessions=U heeft te veel actieve sessies
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatisch censureren
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Du er allerede innlogget på
|
||||
login.alreadyLoggedIn2=enheter. Logg ut og forsøk igjen
|
||||
login.toManySessions=Du har for mange aktive økter
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatisk Sensurering
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Jesteś już zalogowany na
|
||||
login.alreadyLoggedIn2=urządzeniach. Wyloguj się z tych urządzeń i spróbuj ponownie.
|
||||
login.toManySessions=Masz zbyt wiele aktywnych sesji
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatyczne zaciemnienie
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Você já está conectado em
|
||||
login.alreadyLoggedIn2=aparelhos. Por favor saia dos aparelhos e tente novamente.
|
||||
login.toManySessions=Você tem muitas sessões ativas
|
||||
login.logoutMessage=Você foi desconectado.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Ocultação de Texto Automática
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Já tem sessão iniciada em
|
||||
login.alreadyLoggedIn2=dispositivos. Por favor termine sessão nesses dispositivos e tente novamente.
|
||||
login.toManySessions=Tem demasiadas sessões ativas
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Redação Automática
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
|
||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||
login.toManySessions=You have too many active sessions
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Redactare Automată
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Вы уже вошли в
|
||||
login.alreadyLoggedIn2=устройств(а). Пожалуйста, выйдите из этих устройств и попробуйте снова.
|
||||
login.toManySessions=У вас слишком много активных сессий
|
||||
login.logoutMessage=Вы вышли из системы.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Автоматическое редактирование
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
|
||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||
login.toManySessions=You have too many active sessions
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatické redigovanie
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Prijavljeni ste že v
|
||||
login.alreadyLoggedIn2=naprave. Odjavite se iz naprav in poskusite znova.
|
||||
login.toManySessions=Imate preveč aktivnih sej
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Samodejno redigiraj
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Već si prijavljen na
|
||||
login.alreadyLoggedIn2=uređaja. Odjavi se sa uređaja i pokušaj ponovo.
|
||||
login.toManySessions=Imaš previše aktivnih sesija
|
||||
login.logoutMessage=Odjavljen si.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Automatsko cenzurisanje
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Du är redan inloggad på
|
||||
login.alreadyLoggedIn2=enheter. Logga ut från enheterna och försök igen.
|
||||
login.toManySessions=Du har för många aktiva sessioner
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Auto-redigera
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=คุณได้เข้าสู่ระบบใน
|
||||
login.alreadyLoggedIn2=อุปกรณ์แล้ว กรุณาออกจากระบบจากอุปกรณ์ที่ใช้งานอยู่แล้ว จากนั้นลองใหม่อีกครั้ง
|
||||
login.toManySessions=คุณมีการเข้าสู่ระบบพร้อมกันเกินกว่ากำหนด
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=ซ่อนข้อมูลอัตโนมัติ
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Zaten şu cihazlarda oturum açılmış:
|
||||
login.alreadyLoggedIn2=Lütfen bu cihazlardan çıkış yaparak tekrar deneyin.
|
||||
login.toManySessions=Çok fazla aktif oturumunuz var
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Otomatik Karartma
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=Ви вже увійшли до
|
||||
login.alreadyLoggedIn2=пристроїв (а). Будь ласка, вийдіть із цих пристроїв і спробуйте знову.
|
||||
login.toManySessions=У вас дуже багато активних сесій
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Автоматичне редагування
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
|
||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||
login.toManySessions=You have too many active sessions
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Tự động biên tập
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=您已经登录到了
|
||||
login.alreadyLoggedIn2=设备,请注销设备后重试。
|
||||
login.toManySessions=你已经有太多的会话了。请注销一些设备后重试。
|
||||
login.logoutMessage=您已退出登录。
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=自动删除
|
||||
|
||||
@@ -908,6 +908,7 @@ login.alreadyLoggedIn=您已經登入了
|
||||
login.alreadyLoggedIn2=部裝置。請先從這些裝置登出後再試一次。
|
||||
login.toManySessions=您有太多使用中的工作階段
|
||||
login.logoutMessage=您已登出。
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=自動塗黑
|
||||
|
||||
@@ -59,12 +59,17 @@ security:
|
||||
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
|
||||
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
|
||||
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
||||
jwt: # This feature is currently under development and not yet fully supported. Do not use in production.
|
||||
persistence: true # Set to 'true' to enable JWT key store
|
||||
enableKeyRotation: true # Set to 'true' to enable key pair rotation
|
||||
enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
|
||||
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days.
|
||||
secureCookie: false # Set to 'true' to use secure cookies for JWTs
|
||||
|
||||
premium:
|
||||
key: 00000000-0000-0000-0000-000000000000
|
||||
enabled: false # Enable license key checks for pro/enterprise features
|
||||
proFeatures:
|
||||
database: true # Enable database features
|
||||
SSOAutoLogin: false
|
||||
CustomMetadata:
|
||||
autoUpdateMetadata: false
|
||||
|
||||
@@ -132,6 +132,13 @@
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.github.ben-manes.caffeine:caffeine",
|
||||
"moduleUrl": "https://github.com/ben-manes/caffeine",
|
||||
"moduleVersion": "3.2.2",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.github.jai-imageio:jai-imageio-core",
|
||||
"moduleUrl": "https://github.com/jai-imageio/jai-imageio-core",
|
||||
@@ -168,7 +175,7 @@
|
||||
{
|
||||
"moduleName": "com.google.errorprone:error_prone_annotations",
|
||||
"moduleUrl": "https://errorprone.info/error_prone_annotations",
|
||||
"moduleVersion": "2.38.0",
|
||||
"moduleVersion": "2.40.0",
|
||||
"moduleLicense": "Apache 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@@ -549,6 +556,27 @@
|
||||
"moduleLicense": "MIT License",
|
||||
"moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.jsonwebtoken:jjwt-api",
|
||||
"moduleUrl": "https://github.com/jwtk/jjwt",
|
||||
"moduleVersion": "0.12.6",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.jsonwebtoken:jjwt-impl",
|
||||
"moduleUrl": "https://github.com/jwtk/jjwt",
|
||||
"moduleVersion": "0.12.6",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.jsonwebtoken:jjwt-jackson",
|
||||
"moduleUrl": "https://github.com/jwtk/jjwt",
|
||||
"moduleVersion": "0.12.6",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.micrometer:micrometer-commons",
|
||||
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
||||
@@ -1507,6 +1535,13 @@
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-cache",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-data-jpa",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
|
||||
@@ -46,10 +46,9 @@ export class DecryptFile {
|
||||
formData.append('password', password);
|
||||
}
|
||||
// Send decryption request
|
||||
const response = await fetch('/api/v1/security/remove-password', {
|
||||
const response = await fetchWithCsrf('/api/v1/security/remove-password', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: csrfToken ? {'X-XSRF-TOKEN': csrfToken} : undefined,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
formData.append('password', password);
|
||||
|
||||
// Use handleSingleDownload to send the request
|
||||
const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData});
|
||||
const decryptionResult = await fetchWithCsrf(removePasswordUrl, {method: 'POST', body: formData});
|
||||
|
||||
if (decryptionResult && decryptionResult.blob) {
|
||||
const decryptedBlob = await decryptionResult.blob();
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
// Authentication utility for cookie-based JWT
|
||||
window.JWTManager = {
|
||||
|
||||
// Logout - clear cookies and redirect to login
|
||||
logout: function() {
|
||||
|
||||
// Clear JWT cookie manually (fallback)
|
||||
document.cookie = 'stirling_jwt=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=None; Secure';
|
||||
|
||||
// Perform logout request to clear server-side session
|
||||
fetch('/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
}).then(response => {
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else {
|
||||
window.location.href = '/login?logout=true';
|
||||
}
|
||||
}).catch(() => {
|
||||
// If logout fails, let server handle it
|
||||
window.location.href = '/logout';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.fetchWithCsrf = async function(url, options = {}) {
|
||||
function getCsrfToken() {
|
||||
const cookieValue = document.cookie
|
||||
@@ -24,5 +50,18 @@ window.fetchWithCsrf = async function(url, options = {}) {
|
||||
fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken;
|
||||
}
|
||||
|
||||
return fetch(url, fetchOptions);
|
||||
// Always include credentials to send JWT cookies
|
||||
fetchOptions.credentials = 'include';
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
// Handle 401 responses (unauthorized)
|
||||
if (response.status === 401) {
|
||||
console.warn('Authentication failed, redirecting to login');
|
||||
window.JWTManager.logout();
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// JWT Authentication Management Script
|
||||
// This script handles cookie-based JWT authentication and page access control
|
||||
|
||||
(function() {
|
||||
// Clean up JWT token from URL parameters after OAuth/Login flows
|
||||
function cleanupTokenFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const hasToken = urlParams.get('jwt') || urlParams.get('token');
|
||||
if (hasToken) {
|
||||
// Clean up URL by removing token parameter
|
||||
// Token should now be set as cookie by server
|
||||
urlParams.delete('jwt');
|
||||
urlParams.delete('token');
|
||||
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize JWT handling when page loads
|
||||
function initializeJWT() {
|
||||
// Clean up any JWT tokens from URL (OAuth flow)
|
||||
cleanupTokenFromUrl();
|
||||
|
||||
// Authentication is handled server-side
|
||||
// If user is not authenticated, server will redirect to login
|
||||
console.log('JWT initialization complete - authentication handled server-side');
|
||||
}
|
||||
|
||||
// No form enhancement needed for cookie-based JWT
|
||||
// Cookies are automatically sent with form submissions
|
||||
function enhanceFormSubmissions() {
|
||||
// Cookie-based JWT is automatically included in form submissions
|
||||
// No additional processing needed
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeJWT();
|
||||
});
|
||||
} else {
|
||||
initializeJWT();
|
||||
}
|
||||
})();
|
||||
@@ -138,5 +138,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
tooltipSetup();
|
||||
setupDropdowns();
|
||||
fixNavbarDropdownStyles();
|
||||
// Setup logout button functionality
|
||||
const logoutButton = document.querySelector('a[href="/logout"]');
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
if (window.JWTManager) {
|
||||
window.JWTManager.logout();
|
||||
} else {
|
||||
// Fallback if JWTManager is not available
|
||||
window.location.href = '/logout';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
window.addEventListener('resize', fixNavbarDropdownStyles);
|
||||
|
||||
@@ -102,7 +102,7 @@ async function fetchEndpointData() {
|
||||
refreshBtn.classList.add('refreshing');
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
const response = await fetch('/api/v1/info/load/all');
|
||||
const response = await fetchWithCsrf('/api/v1/info/load/all');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
@@ -390,8 +390,13 @@
|
||||
key.includes('clientSubmissionOrder') ||
|
||||
key.includes('lastSubmitTime') ||
|
||||
key.includes('lastClientId') ||
|
||||
|
||||
|
||||
key.includes('stirling_jwt') ||
|
||||
key.includes('JSESSIONID') ||
|
||||
key.includes('XSRF-TOKEN') ||
|
||||
key.includes('remember-me') ||
|
||||
key.includes('auth') ||
|
||||
key.includes('token') ||
|
||||
key.includes('session') ||
|
||||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
|
||||
key.includes('pageViews');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
repositories {
|
||||
maven { url = "https://build.shibboleth.net/maven/releases" }
|
||||
}
|
||||
|
||||
ext {
|
||||
jwtVersion = '0.12.6'
|
||||
}
|
||||
|
||||
bootRun {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
spotless {
|
||||
java {
|
||||
target 'src/**/java/**/*.java'
|
||||
@@ -37,14 +43,14 @@ dependencies {
|
||||
api "org.springframework.security:spring-security-core:$springSecuritySamlVersion"
|
||||
api "org.springframework.security:spring-security-web:$springSecuritySamlVersion"
|
||||
api "org.springframework.security:spring-security-config:$springSecuritySamlVersion"
|
||||
api("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.4")
|
||||
api "org.springframework.security:spring-security-oauth2-jose:$springSecuritySamlVersion"
|
||||
api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
|
||||
api 'org.springframework.boot:spring-boot-starter-jetty'
|
||||
api 'org.springframework.boot:spring-boot-starter-security'
|
||||
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
||||
api 'org.springframework.boot:spring-boot-starter-mail'
|
||||
api 'org.springframework.boot:spring-boot-starter-cache'
|
||||
api 'com.github.ben-manes.caffeine:caffeine'
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.35'
|
||||
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
|
||||
|
||||
@@ -54,6 +60,10 @@ dependencies {
|
||||
// implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' // Removed - UI moved to React frontend
|
||||
api 'io.micrometer:micrometer-registry-prometheus'
|
||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.0.0'
|
||||
|
||||
api "io.jsonwebtoken:jjwt-api:$jwtVersion"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion"
|
||||
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
|
||||
runtimeOnly 'org.postgresql:postgresql:42.7.7'
|
||||
constraints {
|
||||
|
||||
+33
-18
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
@@ -17,6 +18,8 @@ import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
@@ -24,13 +27,17 @@ import stirling.software.proprietary.security.service.UserService;
|
||||
public class CustomAuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
private UserService userService;
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
private final UserService userService;
|
||||
private final JwtServiceInterface jwtService;
|
||||
|
||||
public CustomAuthenticationSuccessHandler(
|
||||
LoginAttemptService loginAttemptService, UserService userService) {
|
||||
LoginAttemptService loginAttemptService,
|
||||
UserService userService,
|
||||
JwtServiceInterface jwtService) {
|
||||
this.loginAttemptService = loginAttemptService;
|
||||
this.userService = userService;
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,23 +53,31 @@ public class CustomAuthenticationSuccessHandler
|
||||
}
|
||||
loginAttemptService.loginSucceeded(userName);
|
||||
|
||||
// Get the saved request
|
||||
HttpSession session = request.getSession(false);
|
||||
SavedRequest savedRequest =
|
||||
(session != null)
|
||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||
: null;
|
||||
if (jwtService.isJwtEnabled()) {
|
||||
String jwt =
|
||||
jwtService.generateToken(
|
||||
authentication, Map.of("authType", AuthenticationType.WEB));
|
||||
jwtService.addToken(response, jwt);
|
||||
log.debug("JWT generated for user: {}", userName);
|
||||
|
||||
if (savedRequest != null
|
||||
&& !RequestUriUtils.isStaticResource(
|
||||
request.getContextPath(), savedRequest.getRedirectUrl())) {
|
||||
// Redirect to the original destination
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
// Redirect to the root URL (considering context path)
|
||||
getRedirectStrategy().sendRedirect(request, response, "/");
|
||||
}
|
||||
} else {
|
||||
// Get the saved request
|
||||
HttpSession session = request.getSession(false);
|
||||
SavedRequest savedRequest =
|
||||
(session != null)
|
||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||
: null;
|
||||
|
||||
// super.onAuthenticationSuccess(request, response, authentication);
|
||||
if (savedRequest != null
|
||||
&& !RequestUriUtils.isStaticResource(
|
||||
request.getContextPath(), savedRequest.getRedirectUrl())) {
|
||||
// Redirect to the original destination
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
// No saved request or it's a static resource, redirect to home page
|
||||
getRedirectStrategy().sendRedirect(request, response, "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -33,6 +33,7 @@ import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
import stirling.software.proprietary.security.saml2.CertificateUtils;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -40,15 +41,18 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
|
||||
public static final String LOGOUT_PATH = "/login?logout=true";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
|
||||
private final AppConfig appConfig;
|
||||
|
||||
private final JwtServiceInterface jwtService;
|
||||
|
||||
@Override
|
||||
@Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC)
|
||||
public void onLogoutSuccess(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException {
|
||||
|
||||
if (!response.isCommitted()) {
|
||||
if (authentication != null) {
|
||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||
@@ -67,6 +71,9 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
authentication.getClass().getSimpleName());
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
} else if (!jwtService.extractToken(request).isBlank()) {
|
||||
jwtService.clearToken(response);
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
} else {
|
||||
// Redirect to login page after logout
|
||||
String path = checkForErrors(request);
|
||||
@@ -82,7 +89,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
Saml2Authentication samlAuthentication)
|
||||
throws IOException {
|
||||
|
||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||
SAML2 samlConf = securityProperties.getSaml2();
|
||||
String registrationId = samlConf.getRegistrationId();
|
||||
|
||||
CustomSaml2AuthenticatedPrincipal principal =
|
||||
@@ -127,7 +134,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
OAuth2AuthenticationToken oAuthToken)
|
||||
throws IOException {
|
||||
String registrationId;
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||
OAUTH2 oauth = securityProperties.getOauth2();
|
||||
String path = checkForErrors(request);
|
||||
|
||||
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
|
||||
|
||||
-1
@@ -43,7 +43,6 @@ public class InitialSecuritySetup {
|
||||
}
|
||||
}
|
||||
|
||||
userService.migrateOauth2ToSSO();
|
||||
assignUsersToDefaultTeamIfMissing();
|
||||
initializeInternalApiUser();
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException authException)
|
||||
throws IOException {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
|
||||
}
|
||||
}
|
||||
+11
@@ -7,3 +7,14 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RateLimitResetScheduler {
|
||||
|
||||
private final IPRateLimitingFilter rateLimitingFilter;
|
||||
|
||||
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
|
||||
public void resetRateLimit() {
|
||||
rateLimitingFilter.resetRequestCounts();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -74,8 +74,11 @@ public class AccountWebController {
|
||||
|
||||
// @GetMapping("/login")
|
||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
// If the user is already authenticated, redirect them to the home page.
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
// If the user is already authenticated and it's not a logout scenario, redirect them to the
|
||||
// home page.
|
||||
if (authentication != null
|
||||
&& authentication.isAuthenticated()
|
||||
&& request.getParameter("logout") == null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@@ -181,7 +184,7 @@ public class AccountWebController {
|
||||
errorOAuth = "login.relyingPartyRegistrationNotFound";
|
||||
// Valid InResponseTo was not available from the validation context, unable to
|
||||
// evaluate
|
||||
case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to";
|
||||
case "invalid_in_response_to" -> errorOAuth = "login.invalidInResponseTo";
|
||||
case "not_authentication_provider_found" ->
|
||||
errorOAuth = "login.not_authentication_provider_found";
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Value("${security.jwt.keyRetentionDays}")
|
||||
private int keyRetentionDays;
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||
cacheManager.setCaffeine(
|
||||
Caffeine.newBuilder()
|
||||
.maximumSize(1000) // Make configurable?
|
||||
.expireAfterWrite(Duration.ofDays(keyRetentionDays))
|
||||
.recordStats());
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
-311
@@ -1,311 +0,0 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Security configuration for Supabase-issued JWTs only.
|
||||
*
|
||||
* Requires:
|
||||
*
|
||||
* spring:
|
||||
* security:
|
||||
* oauth2:
|
||||
* resourceserver:
|
||||
* jwt:
|
||||
* issuer-uri: https://<project-id>.supabase.co/auth/v1
|
||||
*
|
||||
* Optional logging (application.yml):
|
||||
* logging:
|
||||
* level:
|
||||
* org.springframework.security: DEBUG
|
||||
* your.project.pkg.security: DEBUG
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
|
||||
|
||||
/** Your Supabase project ref, e.g. abcd1234efgh5678ijkl */
|
||||
@Value("${app.supabase.project-ref:nrlkjfznsavsbmweiyqu}")
|
||||
private String projectRef;
|
||||
|
||||
/** Optional audience to enforce (leave empty to skip) */
|
||||
@Value("${app.jwt.expected-aud:}")
|
||||
private String expectedAud;
|
||||
|
||||
/** Clock skew in seconds for exp validation */
|
||||
@Value("${app.jwt.clock-skew-seconds:120}")
|
||||
private long clockSkewSeconds;
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http, JwtDecoder jwtDecoder) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(Customizer.withDefaults())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// allow CORS preflight only
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
// public endpoints
|
||||
.requestMatchers(
|
||||
new AntPathRequestMatcher("/actuator/health"),
|
||||
new AntPathRequestMatcher("/public/**"),
|
||||
new AntPathRequestMatcher("/images/**"),
|
||||
new AntPathRequestMatcher("/css/**"),
|
||||
new AntPathRequestMatcher("/js/**")
|
||||
).permitAll()
|
||||
// everything else requires auth
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new VerboseAuthLoggingFilter(), BearerTokenAuthenticationFilter.class)
|
||||
.exceptionHandling(ex -> ex
|
||||
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
|
||||
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
|
||||
)
|
||||
.oauth2ResourceServer(oauth -> oauth
|
||||
.jwt(jwt -> jwt
|
||||
.decoder(jwtDecoder)
|
||||
.jwtAuthenticationConverter(this::toAuthentication)
|
||||
)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/** CORS config so browser can send Authorization on real requests. */
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration cfg = new CorsConfiguration();
|
||||
cfg.setAllowedOrigins(List.of(
|
||||
"http://localhost:3000", // dev
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8080",
|
||||
"https://your-frontend.example"// prod
|
||||
));
|
||||
cfg.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
|
||||
cfg.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With","Accept","Origin"));
|
||||
cfg.setExposedHeaders(List.of("WWW-Authenticate"));
|
||||
cfg.setAllowCredentials(true);
|
||||
cfg.setMaxAge(3600L);
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", cfg);
|
||||
return source;
|
||||
}
|
||||
|
||||
/** JWKS-based decoder + custom validator (issuer/exp/aud). */
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
String issuer = "https://" + projectRef + ".supabase.co/auth/v1"; // no trailing slash
|
||||
String jwks = issuer + "/.well-known/jwks.json";
|
||||
|
||||
log.info("Configuring JWT decoder with JWKS: {}", jwks);
|
||||
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwks).build();
|
||||
decoder.setJwtValidator(new CompositeValidator(issuer, expectedAud, Duration.ofSeconds(clockSkewSeconds)));
|
||||
|
||||
if (expectedAud == null || expectedAud.isBlank()) {
|
||||
log.info("JWT validation: enforcing issuer='{}' and exp (skew {}s)", issuer, clockSkewSeconds);
|
||||
} else {
|
||||
log.info("JWT validation: enforcing issuer='{}', audience='{}', and exp (skew {}s)",
|
||||
issuer, expectedAud, clockSkewSeconds);
|
||||
}
|
||||
return decoder;
|
||||
}
|
||||
|
||||
/** Map claims -> authorities. DEBUG: hotmail.com => ROLE_ADMIN. */
|
||||
private AbstractAuthenticationToken toAuthentication(Jwt jwt) {
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
|
||||
// Supabase default role -> ROLE_authenticated (optional but handy)
|
||||
String supabaseRole = jwt.getClaimAsString("role"); // often "authenticated"
|
||||
if (supabaseRole != null && !supabaseRole.isBlank()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + supabaseRole));
|
||||
}
|
||||
|
||||
// Your custom app_role (if you add it via Access Token Hook) -> ROLE_*
|
||||
String appRole = jwt.getClaimAsString("app_role");
|
||||
if (appRole != null && !appRole.isBlank()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + appRole.toUpperCase()));
|
||||
}
|
||||
|
||||
// DEBUG RULE: email domain → admin
|
||||
String email = jwt.getClaimAsString("email");
|
||||
if (email != null && email.toLowerCase(Locale.ROOT).endsWith("@hotmail.com")) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
|
||||
} else {
|
||||
// Give a basic user role for convenience while debugging (optional)
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
}
|
||||
|
||||
// Optional permissions array -> PERM_*
|
||||
List<String> perms = jwt.getClaimAsStringList("permissions");
|
||||
if (perms != null) {
|
||||
perms.stream()
|
||||
.filter(p -> p != null && !p.isBlank())
|
||||
.map(p -> new SimpleGrantedAuthority("PERM_" + p))
|
||||
.forEach(authorities::add);
|
||||
}
|
||||
|
||||
String principalName = (email != null && !email.isBlank()) ? email : jwt.getSubject();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("JWT accepted: sub='{}', email='{}', supabase.role='{}', app_role='{}', permissions={}",
|
||||
jwt.getSubject(), email, supabaseRole, appRole, perms);
|
||||
log.debug("Granted authorities: {}", authorities.stream()
|
||||
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return new JwtAuthenticationToken(jwt, authorities, principalName);
|
||||
}
|
||||
|
||||
/** Logs authentication lifecycle events (success/failure). */
|
||||
@Bean
|
||||
ApplicationListener<AbstractAuthenticationEvent> authenticationEventsLogger() {
|
||||
return event -> {
|
||||
try {
|
||||
if (event.getSource() instanceof AbstractAuthenticationToken auth) {
|
||||
String type = event.getClass().getSimpleName();
|
||||
String name = auth.getName();
|
||||
String authorities = auth.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
|
||||
log.debug("[AuthEvent] {} principal='{}' authorities='{}' details={}",
|
||||
type, name, authorities, auth.getDetails());
|
||||
} else {
|
||||
log.debug("[AuthEvent] {} source={}", event.getClass().getSimpleName(), event.getSource());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to log authentication event", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Super-chatty per-request logger around bearer processing; never logs raw token. */
|
||||
static class VerboseAuthLoggingFilter extends OncePerRequestFilter {
|
||||
private static final Logger flog = LoggerFactory.getLogger(VerboseAuthLoggingFilter.class);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
boolean hasBearer = authHeader != null && authHeader.startsWith("Bearer ");
|
||||
if (flog.isDebugEnabled()) {
|
||||
flog.debug("[REQ] {} {} AuthorizationHeaderPresent={} (token hidden)",
|
||||
request.getMethod(), request.getRequestURI(), hasBearer);
|
||||
}
|
||||
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} catch (InvalidBearerTokenException ibte) {
|
||||
flog.warn("[AUTH] Invalid bearer token: {}", ibte.getMessage());
|
||||
throw ibte;
|
||||
} catch (Exception ex) {
|
||||
flog.error("[AUTH] Unexpected auth error: {}", ex.toString(), ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth instanceof JwtAuthenticationToken jwtAuth) {
|
||||
String authorities = jwtAuth.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
|
||||
flog.debug("[AUTH] OK principal='{}' authorities='{}'", jwtAuth.getName(), authorities);
|
||||
} else {
|
||||
flog.debug("[AUTH] No authentication established");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Validator: issuer == expected, not expired (with skew), optional audience. */
|
||||
static final class CompositeValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private final String expectedIssuer; // not null
|
||||
private final String expectedAudienceOrNull; // may be null/blank
|
||||
private final Duration skew;
|
||||
|
||||
CompositeValidator(String expectedIssuer, String expectedAudienceOrNull, Duration skew) {
|
||||
this.expectedIssuer = Objects.requireNonNull(expectedIssuer);
|
||||
this.expectedAudienceOrNull = (expectedAudienceOrNull != null && !expectedAudienceOrNull.isBlank())
|
||||
? expectedAudienceOrNull : null;
|
||||
this.skew = Objects.requireNonNull(skew);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt token) {
|
||||
List<OAuth2Error> errors = new ArrayList<>();
|
||||
|
||||
String iss = token.getIssuer() != null ? token.getIssuer().toString() : null;
|
||||
if (iss == null || !iss.equals(expectedIssuer)) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Invalid issuer: " + iss, null));
|
||||
}
|
||||
|
||||
Instant exp = token.getExpiresAt();
|
||||
if (exp == null) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Missing exp claim", null));
|
||||
} else if (exp.isBefore(Instant.now().minus(skew))) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Token expired at " + exp, null));
|
||||
}
|
||||
|
||||
if (expectedAudienceOrNull != null) {
|
||||
List<String> aud = token.getAudience();
|
||||
if (aud == null || !aud.contains(expectedAudienceOrNull)) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Missing/invalid audience: " + expectedAudienceOrNull, null));
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
errors.forEach(e -> log.warn("JWT validation error: {} - {}", e.getErrorCode(), e.getDescription()));
|
||||
return OAuth2TokenValidatorResult.failure(errors);
|
||||
}
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
}
|
||||
}
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.CustomAuthenticationFailureHandler;
|
||||
import stirling.software.proprietary.security.CustomAuthenticationSuccessHandler;
|
||||
import stirling.software.proprietary.security.CustomLogoutSuccessHandler;
|
||||
import stirling.software.proprietary.security.JwtAuthenticationEntryPoint;
|
||||
import stirling.software.proprietary.security.database.repository.JPATokenRepositoryImpl;
|
||||
import stirling.software.proprietary.security.database.repository.PersistentLoginRepository;
|
||||
import stirling.software.proprietary.security.filter.FirstLoginFilter;
|
||||
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
|
||||
import stirling.software.proprietary.security.filter.JwtAuthenticationFilter;
|
||||
import stirling.software.proprietary.security.filter.UserAuthenticationFilter;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
||||
import stirling.software.proprietary.security.service.CustomOAuth2UserService;
|
||||
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@DependsOn("runningProOrHigher")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
private final UserService userService;
|
||||
private final boolean loginEnabledValue;
|
||||
private final boolean runningProOrHigher;
|
||||
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
private final AppConfig appConfig;
|
||||
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||
private final JwtServiceInterface jwtService;
|
||||
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
private final FirstLoginFilter firstLoginFilter;
|
||||
private final SessionPersistentRegistry sessionRegistry;
|
||||
private final PersistentLoginRepository persistentLoginRepository;
|
||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||
|
||||
public SecurityConfiguration(
|
||||
PersistentLoginRepository persistentLoginRepository,
|
||||
CustomUserDetailsService userDetailsService,
|
||||
@Lazy UserService userService,
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||
AppConfig appConfig,
|
||||
ApplicationProperties.Security securityProperties,
|
||||
UserAuthenticationFilter userAuthenticationFilter,
|
||||
JwtServiceInterface jwtService,
|
||||
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
|
||||
LoginAttemptService loginAttemptService,
|
||||
FirstLoginFilter firstLoginFilter,
|
||||
SessionPersistentRegistry sessionRegistry,
|
||||
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
|
||||
@Autowired(required = false)
|
||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||
@Autowired(required = false)
|
||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.userService = userService;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
this.appConfig = appConfig;
|
||||
this.securityProperties = securityProperties;
|
||||
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||
this.jwtService = jwtService;
|
||||
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
|
||||
this.loginAttemptService = loginAttemptService;
|
||||
this.firstLoginFilter = firstLoginFilter;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.persistentLoginRepository = persistentLoginRepository;
|
||||
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
if (securityProperties.getCsrfDisabled() || !loginEnabledValue) {
|
||||
http.csrf(CsrfConfigurer::disable);
|
||||
}
|
||||
|
||||
if (loginEnabledValue) {
|
||||
boolean v2Enabled = appConfig.v2Enabled();
|
||||
|
||||
if (v2Enabled) {
|
||||
http.addFilterBefore(
|
||||
jwtAuthenticationFilter(),
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.exceptionHandling(
|
||||
exceptionHandling ->
|
||||
exceptionHandling.authenticationEntryPoint(
|
||||
jwtAuthenticationEntryPoint));
|
||||
}
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterAfter(rateLimitingFilter(), UserAuthenticationFilter.class)
|
||||
.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
if (!securityProperties.getCsrfDisabled()) {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler =
|
||||
new CsrfTokenRequestAttributeHandler();
|
||||
requestHandler.setCsrfRequestAttributeName(null);
|
||||
http.csrf(
|
||||
csrf ->
|
||||
csrf.ignoringRequestMatchers(
|
||||
request -> {
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
// If there's no API key, don't ignore CSRF
|
||||
// (return false)
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// Validate API key using existing UserService
|
||||
try {
|
||||
Optional<User> user =
|
||||
userService.getUserByApiKey(apiKey);
|
||||
// If API key is valid, ignore CSRF (return
|
||||
// true)
|
||||
// If API key is invalid, don't ignore CSRF
|
||||
// (return false)
|
||||
return user.isPresent();
|
||||
} catch (Exception e) {
|
||||
// If there's any error validating the API
|
||||
// key, don't ignore CSRF
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.csrfTokenRepository(cookieRepo)
|
||||
.csrfTokenRequestHandler(requestHandler));
|
||||
}
|
||||
|
||||
http.sessionManagement(
|
||||
sessionManagement -> {
|
||||
if (v2Enabled) {
|
||||
sessionManagement.sessionCreationPolicy(
|
||||
SessionCreationPolicy.STATELESS);
|
||||
} else {
|
||||
sessionManagement
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
.maximumSessions(10)
|
||||
.maxSessionsPreventsLogin(false)
|
||||
.sessionRegistry(sessionRegistry)
|
||||
.expiredUrl("/login?logout=true");
|
||||
}
|
||||
});
|
||||
http.authenticationProvider(daoAuthenticationProvider());
|
||||
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
||||
http.logout(
|
||||
logout ->
|
||||
logout.logoutRequestMatcher(
|
||||
PathPatternRequestMatcher.withDefaults()
|
||||
.matcher("/logout"))
|
||||
.logoutSuccessHandler(
|
||||
new CustomLogoutSuccessHandler(
|
||||
securityProperties, appConfig, jwtService))
|
||||
.clearAuthentication(true)
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "remember-me", "stirling_jwt"));
|
||||
http.rememberMe(
|
||||
rememberMeConfigurer -> // Use the configurator directly
|
||||
rememberMeConfigurer
|
||||
.tokenRepository(persistentTokenRepository())
|
||||
.tokenValiditySeconds( // 14 days
|
||||
14 * 24 * 60 * 60)
|
||||
.userDetailsService( // Your existing UserDetailsService
|
||||
userDetailsService)
|
||||
.useSecureCookie( // Enable secure cookie
|
||||
true)
|
||||
.rememberMeParameter( // Form parameter name
|
||||
"remember-me")
|
||||
.rememberMeCookieName( // Cookie name
|
||||
"remember-me")
|
||||
.alwaysRemember(false));
|
||||
http.authorizeHttpRequests(
|
||||
authz ->
|
||||
authz.requestMatchers(
|
||||
req -> {
|
||||
String uri = req.getRequestURI();
|
||||
String contextPath = req.getContextPath();
|
||||
|
||||
// Remove the context path from the URI
|
||||
String trimmedUri =
|
||||
uri.startsWith(contextPath)
|
||||
? uri.substring(
|
||||
contextPath.length())
|
||||
: uri;
|
||||
return trimmedUri.startsWith("/login")
|
||||
|| trimmedUri.startsWith("/oauth")
|
||||
|| trimmedUri.startsWith("/saml2")
|
||||
|| trimmedUri.endsWith(".svg")
|
||||
|| trimmedUri.startsWith("/register")
|
||||
|| trimmedUri.startsWith("/error")
|
||||
|| trimmedUri.startsWith("/images/")
|
||||
|| trimmedUri.startsWith("/public/")
|
||||
|| trimmedUri.startsWith("/css/")
|
||||
|| trimmedUri.startsWith("/fonts/")
|
||||
|| trimmedUri.startsWith("/js/")
|
||||
|| trimmedUri.startsWith("/pdfjs/")
|
||||
|| trimmedUri.startsWith("/pdfjs-legacy/")
|
||||
|| trimmedUri.startsWith("/favicon")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/info/status")
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
|| uri.contains("/v1/api-docs");
|
||||
})
|
||||
.permitAll()
|
||||
.anyRequest()
|
||||
.authenticated());
|
||||
// Handle User/Password Logins
|
||||
if (securityProperties.isUserPass()) {
|
||||
http.formLogin(
|
||||
formLogin ->
|
||||
formLogin
|
||||
.loginPage("/login")
|
||||
.successHandler(
|
||||
new CustomAuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
userService,
|
||||
jwtService))
|
||||
.failureHandler(
|
||||
new CustomAuthenticationFailureHandler(
|
||||
loginAttemptService, userService))
|
||||
.permitAll());
|
||||
}
|
||||
// Handle OAUTH2 Logins
|
||||
if (securityProperties.isOauth2Active()) {
|
||||
http.oauth2Login(
|
||||
oauth2 ->
|
||||
oauth2.loginPage("/oauth2")
|
||||
/*
|
||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
|
||||
is set as true, else login fails with an error message advising the same.
|
||||
*/
|
||||
.successHandler(
|
||||
new CustomOAuth2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
securityProperties.getOauth2(),
|
||||
userService,
|
||||
jwtService))
|
||||
.failureHandler(
|
||||
new CustomOAuth2AuthenticationFailureHandler())
|
||||
// Add existing Authorities from the database
|
||||
.userInfoEndpoint(
|
||||
userInfoEndpoint ->
|
||||
userInfoEndpoint
|
||||
.oidcUserService(
|
||||
new CustomOAuth2UserService(
|
||||
securityProperties,
|
||||
userService,
|
||||
loginAttemptService))
|
||||
.userAuthoritiesMapper(
|
||||
oAuth2userAuthoritiesMapper))
|
||||
.permitAll());
|
||||
}
|
||||
// Handle SAML
|
||||
if (securityProperties.isSaml2Active() && runningProOrHigher) {
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
http.authenticationProvider(authenticationProvider)
|
||||
.saml2Login(
|
||||
saml2 -> {
|
||||
try {
|
||||
saml2.loginPage("/saml2")
|
||||
.relyingPartyRegistrationRepository(
|
||||
saml2RelyingPartyRegistrations)
|
||||
.authenticationManager(
|
||||
new ProviderManager(authenticationProvider))
|
||||
.successHandler(
|
||||
new CustomSaml2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
securityProperties.getSaml2(),
|
||||
userService,
|
||||
jwtService))
|
||||
.failureHandler(
|
||||
new CustomSaml2AuthenticationFailureHandler())
|
||||
.authenticationRequestResolver(
|
||||
saml2AuthenticationRequestResolver);
|
||||
} catch (Exception e) {
|
||||
log.error("Error configuring SAML 2 login", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.debug("Login is not enabled.");
|
||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
}
|
||||
return http.build();
|
||||
}
|
||||
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
|
||||
provider.setPasswordEncoder(passwordEncoder());
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IPRateLimitingFilter rateLimitingFilter() {
|
||||
// Example limit TODO add config level
|
||||
int maxRequestsPerIp = 1000000;
|
||||
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
return new JPATokenRepositoryImpl(persistentLoginRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationFilter jwtAuthenticationFilter() {
|
||||
return new JwtAuthenticationFilter(
|
||||
jwtService,
|
||||
userService,
|
||||
userDetailsService,
|
||||
jwtAuthenticationEntryPoint,
|
||||
securityProperties);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -43,7 +43,7 @@ import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequ
|
||||
@Tag(name = "Admin Settings", description = "Admin-only Settings Management APIs")
|
||||
@RequestMapping("/api/v1/admin/settings")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Slf4j
|
||||
public class AdminSettingsController {
|
||||
|
||||
|
||||
+9
-3
@@ -105,7 +105,9 @@ public class UserController {
|
||||
if (user.getUsername().equals(newUsername)) {
|
||||
return new RedirectView("/account?messageType=usernameExists", true);
|
||||
}
|
||||
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||
}
|
||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||
return new RedirectView("/account?messageType=usernameExists", true);
|
||||
}
|
||||
@@ -139,7 +141,9 @@ public class UserController {
|
||||
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/change-creds?messageType=incorrectPassword", true);
|
||||
}
|
||||
userService.changePassword(user, newPassword);
|
||||
userService.changeFirstUse(user, false);
|
||||
// Logout using Spring's utility
|
||||
@@ -165,7 +169,9 @@ public class UserController {
|
||||
return new RedirectView("/account?messageType=userNotFound", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||
}
|
||||
userService.changePassword(user, newPassword);
|
||||
// Logout using Spring's utility
|
||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package stirling.software.proprietary.security.filter;
|
||||
|
||||
import static stirling.software.common.util.RequestUriUtils.isStaticResource;
|
||||
import static stirling.software.proprietary.security.model.AuthenticationType.*;
|
||||
import static stirling.software.proprietary.security.model.AuthenticationType.SAML2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
@Slf4j
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtServiceInterface jwtService;
|
||||
private final UserService userService;
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
private final AuthenticationEntryPoint authenticationEntryPoint;
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
|
||||
public JwtAuthenticationFilter(
|
||||
JwtServiceInterface jwtService,
|
||||
UserService userService,
|
||||
CustomUserDetailsService userDetailsService,
|
||||
AuthenticationEntryPoint authenticationEntryPoint,
|
||||
ApplicationProperties.Security securityProperties) {
|
||||
this.jwtService = jwtService;
|
||||
this.userService = userService;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.authenticationEntryPoint = authenticationEntryPoint;
|
||||
this.securityProperties = securityProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
if (!jwtService.isJwtEnabled()) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
if (isStaticResource(request.getContextPath(), request.getRequestURI())) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKeyExists(request, response)) {
|
||||
String jwtToken = jwtService.extractToken(request);
|
||||
|
||||
if (jwtToken == null) {
|
||||
// Any unauthenticated requests should redirect to /login
|
||||
String requestURI = request.getRequestURI();
|
||||
String contextPath = request.getContextPath();
|
||||
|
||||
if (!requestURI.startsWith(contextPath + "/login")) {
|
||||
response.sendRedirect("/login");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
jwtService.validateToken(jwtToken);
|
||||
} catch (AuthenticationFailureException e) {
|
||||
jwtService.clearToken(response);
|
||||
handleAuthenticationFailure(request, response, e);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> claims = jwtService.extractClaims(jwtToken);
|
||||
String tokenUsername = claims.get("sub").toString();
|
||||
|
||||
try {
|
||||
authenticate(request, claims);
|
||||
} catch (SQLException | UnsupportedProviderException e) {
|
||||
log.error("Error processing user authentication for user: {}", tokenUsername, e);
|
||||
handleAuthenticationFailure(
|
||||
request,
|
||||
response,
|
||||
new AuthenticationFailureException(
|
||||
"Error processing user authentication", e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private boolean apiKeyExists(HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
try {
|
||||
Optional<User> user = userService.getUserByApiKey(apiKey);
|
||||
|
||||
if (user.isEmpty()) {
|
||||
handleAuthenticationFailure(
|
||||
request,
|
||||
response,
|
||||
new AuthenticationFailureException("Invalid API Key"));
|
||||
return false;
|
||||
}
|
||||
|
||||
authentication =
|
||||
new ApiKeyAuthenticationToken(
|
||||
user.get(), apiKey, user.get().getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return true;
|
||||
} catch (AuthenticationException e) {
|
||||
handleAuthenticationFailure(
|
||||
request,
|
||||
response,
|
||||
new AuthenticationFailureException("Invalid API Key", e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void authenticate(HttpServletRequest request, Map<String, Object> claims)
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
String username = claims.get("sub").toString();
|
||||
|
||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
processUserAuthenticationType(claims, username);
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (userDetails != null) {
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
} else {
|
||||
throw new UsernameNotFoundException("User not found: " + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processUserAuthenticationType(Map<String, Object> claims, String username)
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
AuthenticationType authenticationType =
|
||||
AuthenticationType.valueOf(claims.getOrDefault("authType", WEB).toString());
|
||||
log.debug("Processing {} login for {} user", authenticationType, username);
|
||||
|
||||
switch (authenticationType) {
|
||||
case OAUTH2 -> {
|
||||
ApplicationProperties.Security.OAUTH2 oauth2Properties =
|
||||
securityProperties.getOauth2();
|
||||
userService.processSSOPostLogin(
|
||||
username, oauth2Properties.getAutoCreateUser(), OAUTH2);
|
||||
}
|
||||
case SAML2 -> {
|
||||
ApplicationProperties.Security.SAML2 saml2Properties =
|
||||
securityProperties.getSaml2();
|
||||
userService.processSSOPostLogin(
|
||||
username, saml2Properties.getAutoCreateUser(), SAML2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException authException)
|
||||
throws IOException, ServletException {
|
||||
authenticationEntryPoint.commence(request, response, authException);
|
||||
}
|
||||
}
|
||||
+14
-18
@@ -9,7 +9,6 @@ import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
@@ -64,6 +63,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
// Check for session expiration (unsure if needed)
|
||||
@@ -92,14 +92,9 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
response.getWriter().write("Invalid API Key.");
|
||||
return;
|
||||
}
|
||||
List<SimpleGrantedAuthority> authorities =
|
||||
user.get().getAuthorities().stream()
|
||||
.map(
|
||||
authority ->
|
||||
new SimpleGrantedAuthority(
|
||||
authority.getAuthority()))
|
||||
.toList();
|
||||
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
|
||||
authentication =
|
||||
new ApiKeyAuthenticationToken(
|
||||
user.get(), apiKey, user.get().getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} catch (AuthenticationException e) {
|
||||
// If API key authentication fails, deny the request
|
||||
@@ -115,20 +110,19 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
String method = request.getMethod();
|
||||
String contextPath = request.getContextPath();
|
||||
|
||||
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
|
||||
if ("GET".equalsIgnoreCase(method) && !requestURI.startsWith(contextPath + "/login")) {
|
||||
response.sendRedirect(contextPath + "/login"); // redirect to the login page
|
||||
return;
|
||||
} else {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter()
|
||||
.write(
|
||||
"Authentication required. Please provide a X-API-KEY in request"
|
||||
+ " header.\n"
|
||||
+ "This is found in Settings -> Account Settings -> API Key\n"
|
||||
+ "Alternatively you can disable authentication if this is"
|
||||
+ " unexpected");
|
||||
return;
|
||||
"""
|
||||
Authentication required. Please provide a X-API-KEY in request header.
|
||||
This is found in Settings -> Account Settings -> API Key
|
||||
Alternatively you can disable authentication if this is unexpected.
|
||||
""");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the authenticated user is disabled and invalidate their session if so
|
||||
@@ -226,11 +220,12 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
String contextPath = request.getContextPath();
|
||||
String[] permitAllPatterns = {
|
||||
contextPath + "/login",
|
||||
contextPath + "/signup",
|
||||
contextPath + "/register",
|
||||
contextPath + "/error",
|
||||
contextPath + "/images/",
|
||||
@@ -247,6 +242,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
for (String pattern : permitAllPatterns) {
|
||||
if (uri.startsWith(pattern)
|
||||
|| uri.endsWith(".svg")
|
||||
|| uri.endsWith(".mjs")
|
||||
|| uri.endsWith(".png")
|
||||
|| uri.endsWith(".ico")) {
|
||||
return true;
|
||||
|
||||
+4
-1
@@ -2,5 +2,8 @@ package stirling.software.proprietary.security.model;
|
||||
|
||||
public enum AuthenticationType {
|
||||
WEB,
|
||||
SSO
|
||||
@Deprecated(since = "1.0.2")
|
||||
SSO,
|
||||
OAUTH2,
|
||||
SAML2
|
||||
}
|
||||
|
||||
+3
-1
@@ -2,6 +2,8 @@ package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
@@ -18,7 +20,7 @@ import lombok.Setter;
|
||||
@Table(name = "authorities")
|
||||
@Getter
|
||||
@Setter
|
||||
public class Authority implements Serializable {
|
||||
public class Authority implements GrantedAuthority, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString(onlyExplicitlyIncluded = true)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class JwtVerificationKey implements Serializable {
|
||||
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@ToString.Include private String keyId;
|
||||
|
||||
private String verifyingKey;
|
||||
|
||||
@ToString.Include private LocalDateTime createdAt;
|
||||
|
||||
public JwtVerificationKey(String keyId, String verifyingKey) {
|
||||
this.keyId = keyId;
|
||||
this.verifyingKey = verifyingKey;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -7,6 +7,8 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -25,7 +27,7 @@ import stirling.software.proprietary.model.Team;
|
||||
@Setter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
@ToString(onlyExplicitlyIncluded = true)
|
||||
public class User implements Serializable {
|
||||
public class User implements UserDetails, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package stirling.software.proprietary.security.model.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
public class AuthenticationFailureException extends AuthenticationException {
|
||||
public AuthenticationFailureException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthenticationFailureException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
+19
-9
@@ -1,7 +1,11 @@
|
||||
package stirling.software.proprietary.security.oauth2;
|
||||
|
||||
import static stirling.software.proprietary.security.model.AuthenticationType.OAUTH2;
|
||||
import static stirling.software.proprietary.security.model.AuthenticationType.SSO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -18,10 +22,10 @@ import jakarta.servlet.http.HttpSession;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
@@ -30,8 +34,9 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final ApplicationProperties.Security.OAUTH2 oauth2Properties;
|
||||
private final UserService userService;
|
||||
private final JwtServiceInterface jwtService;
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(
|
||||
@@ -60,8 +65,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
// Redirect to the original destination
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
OAUTH2 oAuth = applicationProperties.getSecurity().getOauth2();
|
||||
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
if (session != null) {
|
||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||
@@ -69,7 +72,12 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
throw new LockedException(
|
||||
"Your account has been locked due to too many failed login attempts.");
|
||||
}
|
||||
|
||||
if (jwtService.isJwtEnabled()) {
|
||||
String jwt =
|
||||
jwtService.generateToken(
|
||||
authentication, Map.of("authType", AuthenticationType.OAUTH2));
|
||||
jwtService.addToken(response, jwt);
|
||||
}
|
||||
if (userService.isUserDisabled(username)) {
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||
@@ -77,20 +85,22 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
}
|
||||
if (userService.usernameExistsIgnoreCase(username)
|
||||
&& userService.hasPassword(username)
|
||||
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
||||
&& oAuth.getAutoCreateUser()) {
|
||||
&& (!userService.isAuthenticationTypeByUsername(username, SSO)
|
||||
|| !userService.isAuthenticationTypeByUsername(username, OAUTH2))
|
||||
&& oauth2Properties.getAutoCreateUser()) {
|
||||
response.sendRedirect(contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (oAuth.getBlockRegistration()
|
||||
if (oauth2Properties.getBlockRegistration()
|
||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
||||
return;
|
||||
}
|
||||
if (principal instanceof OAuth2User) {
|
||||
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
||||
userService.processSSOPostLogin(
|
||||
username, oauth2Properties.getAutoCreateUser(), OAUTH2);
|
||||
}
|
||||
response.sendRedirect(contextPath + "/");
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
|
||||
+9
-6
@@ -34,6 +34,7 @@ import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
import stirling.software.common.model.oauth2.Provider;
|
||||
import stirling.software.proprietary.security.model.Authority;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.model.exception.NoProviderFoundException;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
@@ -239,12 +240,14 @@ public class OAuth2Configuration {
|
||||
Optional<User> userOpt =
|
||||
userService.findByUsernameIgnoreCase(
|
||||
(String) oAuth2Auth.getAttributes().get(useAsUsername));
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
mappedAuthorities.add(
|
||||
new SimpleGrantedAuthority(
|
||||
userService.findRole(user).getAuthority()));
|
||||
}
|
||||
userOpt.ifPresent(
|
||||
user ->
|
||||
mappedAuthorities.add(
|
||||
new Authority(
|
||||
userService
|
||||
.findRole(user)
|
||||
.getAuthority(),
|
||||
user)));
|
||||
}
|
||||
});
|
||||
return mappedAuthorities;
|
||||
|
||||
+32
-12
@@ -1,7 +1,11 @@
|
||||
package stirling.software.proprietary.security.saml2;
|
||||
|
||||
import static stirling.software.proprietary.security.model.AuthenticationType.SAML2;
|
||||
import static stirling.software.proprietary.security.model.AuthenticationType.SSO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -17,10 +21,10 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
@@ -30,8 +34,9 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
private ApplicationProperties applicationProperties;
|
||||
private ApplicationProperties.Security.SAML2 saml2Properties;
|
||||
private UserService userService;
|
||||
private final JwtServiceInterface jwtService;
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(
|
||||
@@ -65,10 +70,9 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
savedRequest.getRedirectUrl());
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
||||
log.debug(
|
||||
"Processing SAML2 authentication with autoCreateUser: {}",
|
||||
saml2.getAutoCreateUser());
|
||||
saml2Properties.getAutoCreateUser());
|
||||
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
log.debug("User {} is blocked due to too many login attempts", username);
|
||||
@@ -82,17 +86,21 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
||||
boolean hasPassword = userExists && userService.hasPassword(username);
|
||||
boolean isSSOUser =
|
||||
userExists
|
||||
&& userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.SSO);
|
||||
userExists && userService.isAuthenticationTypeByUsername(username, SSO);
|
||||
boolean isSAML2User =
|
||||
userExists && userService.isAuthenticationTypeByUsername(username, SAML2);
|
||||
|
||||
log.debug(
|
||||
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
|
||||
"User status - Exists: {}, Has password: {}, Is SSO user: {}, Is SAML2 user: {}",
|
||||
userExists,
|
||||
hasPassword,
|
||||
isSSOUser);
|
||||
isSSOUser,
|
||||
isSAML2User);
|
||||
|
||||
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
|
||||
if (userExists
|
||||
&& hasPassword
|
||||
&& (!isSSOUser || !isSAML2User)
|
||||
&& saml2Properties.getAutoCreateUser()) {
|
||||
log.debug(
|
||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||
username);
|
||||
@@ -102,15 +110,18 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
}
|
||||
|
||||
try {
|
||||
if (saml2.getBlockRegistration() && !userExists) {
|
||||
if (!userExists || saml2Properties.getBlockRegistration()) {
|
||||
log.debug("Registration blocked for new user: {}", username);
|
||||
response.sendRedirect(
|
||||
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
||||
return;
|
||||
}
|
||||
log.debug("Processing SSO post-login for user: {}", username);
|
||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||
userService.processSSOPostLogin(
|
||||
username, saml2Properties.getAutoCreateUser(), SAML2);
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
|
||||
generateJwt(response, authentication);
|
||||
response.sendRedirect(contextPath + "/");
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
log.debug(
|
||||
@@ -124,4 +135,13 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateJwt(HttpServletResponse response, Authentication authentication) {
|
||||
if (jwtService.isJwtEnabled()) {
|
||||
String jwt =
|
||||
jwtService.generateToken(
|
||||
authentication, Map.of("authType", AuthenticationType.SAML2));
|
||||
jwtService.addToken(response, jwt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user