diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..5e04587dd
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+target/**
+build/**
+bin/**
+.idea/**
+.history/**
+.github/**
+.git/**
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..8fc5677e1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+# Set line endings to LF, even on Windows. Otherwise, execution within Docker fails.
+# See https://help.github.com/articles/dealing-with-line-endings/
+*.sh text eol=lf
+gradlew text eol=lf
+*.cmd text eol=crlf
+*.bat text eol=crlf
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..c8398bda9
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+ - package-ecosystem: gradle
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 000000000..58fa77f03
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,11 @@
+{
+ "extends": [
+ "config:base",
+ ":preserveSemverRanges",
+ ":rebaseStalePrs",
+ ":disableRateLimiting",
+ ":semanticCommits",
+ ":semanticCommitTypeAll(renovatebot)"
+ ],
+ "labels": ["dependencies", "bot"]
+}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..22e05e2ed
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,35 @@
+name: Build
+
+env:
+ JAVA_OPTS: "-Xms512m -Xmx6048m -Xss128m -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC"
+ GRADLE_OPTS: "-Xms512m -Xmx6048m -Xss128m -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC"
+ TERM: xterm-256color
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+concurrency:
+ group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}"
+ cancel-in-progress: ${{ github.event_name == 'push' }}
+
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ ubuntu-latest, macos-latest, windows-latest ]
+ jdk: [ 21 ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK ${{ matrix.jdk }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ matrix.jdk }}
+ distribution: 'corretto'
+ - name: Build with JDK ${{ matrix.jdk }} on ${{ matrix.os }}
+ run: ./gradlew build
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..1d6df4a3b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+.classpath
+!/.project
+.project
+.settings
+.history
+.vscode
+target/
+.idea/
+.DS_Store
+.idea
+overlays/
+.gradle/
+build/
+log/
+bin/
+*.war
+*.iml
+*.log
+tmp/
+./apache-tomcat
+apache-tomcat.zip
+config-metadata.properties
+node-modules
+package-lock.json
\ No newline at end of file
diff --git a/.java-version b/.java-version
new file mode 100644
index 000000000..74623ac8d
--- /dev/null
+++ b/.java-version
@@ -0,0 +1 @@
+21.0
\ No newline at end of file
diff --git a/.sdkmanrc b/.sdkmanrc
new file mode 100644
index 000000000..6b6985e17
--- /dev/null
+++ b/.sdkmanrc
@@ -0,0 +1 @@
+java=21
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..413e8b3a1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,45 @@
+ARG BASE_IMAGE="azul/zulu-openjdk:21"
+
+FROM $BASE_IMAGE AS overlay
+
+ARG EXT_BUILD_COMMANDS=""
+ARG EXT_BUILD_OPTIONS=""
+
+WORKDIR /cas-overlay
+COPY ./src src/
+COPY ./gradle/ gradle/
+COPY ./gradlew ./settings.gradle ./build.gradle ./gradle.properties ./lombok.config ./
+
+RUN mkdir -p ~/.gradle \
+ && echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties \
+ && echo "org.gradle.configureondemand=true" >> ~/.gradle/gradle.properties \
+ && chmod 750 ./gradlew \
+ && ./gradlew --version;
+
+RUN ./gradlew clean build $EXT_BUILD_COMMANDS --parallel --no-daemon -Pexecutable=false $EXT_BUILD_OPTIONS;
+
+RUN java -Djarmode=tools -jar build/libs/cas.war extract \
+ && java -XX:ArchiveClassesAtExit=./cas/cas.jsa -Dspring.context.exit=onRefresh -jar cas/cas.war
+
+FROM $BASE_IMAGE AS cas
+
+LABEL "Organization"="Apereo"
+LABEL "Description"="Apereo CAS"
+
+RUN mkdir -p /etc/cas/config \
+ && mkdir -p /etc/cas/services \
+ && mkdir -p /etc/cas/saml;
+
+WORKDIR cas-overlay
+COPY --from=overlay /cas-overlay/cas cas/
+
+COPY etc/cas/ /etc/cas/
+COPY etc/cas/config/ /etc/cas/config/
+COPY etc/cas/services/ /etc/cas/services/
+COPY etc/cas/saml/ /etc/cas/saml/
+
+EXPOSE 8080 8443
+
+ENV PATH $PATH:$JAVA_HOME/bin:.
+
+ENTRYPOINT ["java", "-server", "-noverify", "-Xmx2048M", "-XX:SharedArchiveFile=cas/cas.jsa", "-jar", "cas/cas.war"]
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Procfile b/Procfile
new file mode 100644
index 000000000..2c732c313
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: java $JAVA_OPTS -jar build/libs/cas.war --server.port=$PORT --server.ssl.enabled=false
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..47753e62e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,373 @@
+Apereo CAS WAR Overlay Template
+=====================================
+
+WAR Overlay Type: `cas-overlay`
+
+# Versions
+
+- CAS Server `7.2.0-RC3`
+- JDK `21`
+
+# Build
+
+To build the project, use:
+
+```bash
+# Use --refresh-dependencies to force-update SNAPSHOT versions
+./gradlew[.bat] clean build
+```
+
+To see what commands/tasks are available to the build script, run:
+
+```bash
+./gradlew[.bat] tasks
+```
+
+If you need to, on Linux/Unix systems, you can delete all the existing artifacts
+(artifacts and metadata) Gradle has downloaded using:
+
+```bash
+# Only do this when absolutely necessary
+rm -rf $HOME/.gradle/caches/
+```
+
+Same strategy applies to Windows too, provided you switch `$HOME` to its equivalent in the above command.
+
+# Keystore
+
+For the server to run successfully, you might need to create a keystore file.
+This can either be done using the JDK's `keytool` utility or via the following command:
+
+```bash
+./gradlew[.bat] createKeystore
+```
+
+Use the password `changeit` for both the keystore and the key/certificate entries.
+Ensure the keystore is loaded up with keys and certificates of the server.
+
+## Extension Modules
+
+Extension modules may be specified under the `dependencies` block of the [Gradle build script](build.gradle):
+
+```gradle
+dependencies {
+ implementation "org.apereo.cas:cas-server-some-module"
+ ...
+}
+```
+
+To collect the list of all project modules and dependencies in the overlay:
+
+```bash
+./gradlew[.bat] dependencies
+```
+
+# Deployment
+
+On a successful deployment via the following methods, the server will be available at:
+
+* `https://localhost:8443/cas`
+
+
+## Executable WAR
+
+Run the server web application as an executable WAR. Note that running an executable WAR requires CAS to use an embedded container such as Apache Tomcat, Jetty, etc.
+
+The current servlet container is specified as `-tomcat`.
+
+```bash
+java -jar build/libs/cas.war
+```
+
+Or via:
+
+```bash
+./gradlew[.bat] run
+```
+
+It is often an advantage to explode the generated web application and run it in unpacked mode.
+One way to run an unpacked archive is by starting the appropriate launcher, as follows:
+
+```bash
+jar -xf build/libs/cas.war
+cd build/libs
+java org.springframework.boot.loader.launch.JarLauncher
+```
+
+This is slightly faster on startup (depending on the size of the WAR file) than
+running from an unexploded archive. After startup, you should not expect any differences.
+
+Debug the CAS web application as an executable WAR:
+
+```bash
+./gradlew[.bat] debug
+```
+
+Or via:
+
+```bash
+java -Xdebug -Xrunjdwp:transport=dt_socket,address=5000,server=y,suspend=y -jar build/libs/cas.war
+```
+
+Run the CAS web application as a *standalone* executable WAR:
+
+```bash
+./gradlew[.bat] clean executable
+```
+
+### CDS Support
+
+CDS is a JVM feature that can help reduce the startup time and memory footprint of Java applications. CAS via Spring Boot
+now has support for easy creation of a CDS friendly layout. This layout can be created by extracting the CAS web application file
+with the help of the `tools` jarmode:
+
+```bash
+# Note:
+# You must first build the web application with "executable" turned off
+java -Djarmode=tools -jar build/libs/cas.war extract
+
+# Perform a training run once
+java -XX:ArchiveClassesAtExit=cas.jsa -Dspring.context.exit=onRefresh -jar cas/cas.war
+
+# Run the CAS web application via CDS
+java XX:SharedArchiveFile=cas.jsa -jar cas/cas.war
+```
+
+## External
+
+Deploy the binary web application file in `build/libs` after a successful build to a servlet container of choice.
+
+# Docker
+
+The following strategies outline how to build and deploy CAS Docker images.
+
+## Jib
+
+The overlay embraces the [Jib Gradle Plugin](https://github.com/GoogleContainerTools/jib) to provide easy-to-use out-of-the-box tooling for building CAS docker images. Jib is an open-source Java containerizer from Google that lets Java developers build containers using the tools they know. It is a container image builder that handles all the steps of packaging your application into a container image. It does not require you to write a Dockerfile or have Docker installed, and it is directly integrated into the overlay.
+
+```bash
+# Running this task requires that you have Docker installed and running.
+./gradlew build jibDockerBuild
+```
+
+## Dockerfile
+
+You can also use the Docker tooling and the provided `Dockerfile` to build and run.
+There are dedicated Gradle tasks available to build and push Docker images using the supplied `DockerFile`:
+
+```bash
+./gradlew build casBuildDockerImage
+```
+
+Once ready, you may also push the images:
+
+```bash
+./gradlew casPushDockerImage
+```
+
+If credentials (username+password) are required for pull and push operations, they may be specified
+using system properties via `-DdockerUsername=...` and `-DdockerPassword=...`.
+
+A `docker-compose.yml` is also provided to orchestrate the build:
+
+```bash
+docker-compose build
+```
+
+
+## Spring Boot
+
+You can use the Spring Boot build plugin for Gradle to create CAS container images.
+The plugins create an OCI image (the same format as one created by docker build)
+by using [Cloud Native Buildpacks](https://buildpacks.io/). You do not need a Dockerfile, but you do need a Docker daemon,
+either locally (which is what you use when you build with docker) or remotely
+through the `DOCKER_HOST` environment variable. The default builder is optimized for
+Spring Boot applications such as CAS, and the image is layered efficiently.
+
+```bash
+./gradlew bootBuildImage
+```
+
+The first build might take a long time because it has to download some container
+images and the JDK, but subsequent builds should be fast.
+
+
+# CAS Command-line Shell
+
+To launch into the CAS command-line shell:
+
+```bash
+./gradlew[.bat] downloadShell runShell
+```
+
+# Retrieve Overlay Resources
+
+To fetch and overlay a CAS resource or view, use:
+
+```bash
+./gradlew[.bat] getResource -PresourceName=[resource-name]
+```
+
+# Create User Interface Themes Structure
+
+You can use the overlay to construct the correct directory structure for custom user interface themes:
+
+```bash
+./gradlew[.bat] createTheme -Ptheme=redbeard
+```
+
+The generated directory structure should match the following:
+
+```
+├── redbeard.properties
+├── static
+│ └── themes
+│ └── redbeard
+│ ├── css
+│ │ └── cas.css
+│ └── js
+│ └── cas.js
+└── templates
+ └── redbeard
+ └── fragments
+```
+
+HTML templates and fragments can be moved into the above directory structure,
+and the theme may be assigned to applications for use.
+
+# List Overlay Resources
+
+To list all available CAS views and templates:
+
+```bash
+./gradlew[.bat] listTemplateViews
+```
+
+To unzip and explode the CAS web application file and the internal resources jar:
+
+```bash
+./gradlew[.bat] explodeWar
+```
+
+# Configuration
+
+- The `etc` directory contains the configuration files and directories that need to be copied to `/etc/cas/config`.
+
+```bash
+./gradlew[.bat] copyCasConfiguration
+```
+
+- The specifics of the build are controlled using the `gradle.properties` file.
+
+## Configuration Metadata
+
+Configuration metadata allows you to export collection of CAS properties as a report into a file
+that can later be examined. You will find a full list of CAS settings along with notes, types, default and accepted values:
+
+```bash
+./gradlew exportConfigMetadata
+```
+
+# Puppeteer
+
+> [Puppeteer](https://pptr.dev/) is a Node.js library which provides a high-level API to control Chrome/Chromium over the DevTools Protocol.
+> Puppeteer runs in headless mode by default, but can be configured to run in full (non-headless) Chrome/Chromium.
+
+Puppeteer scenarios, used here as a form of acceptance testing, allow you to verify CAS functionality to address a particular authentication flow. The scenarios, which may be
+found inside the `./puppeteer/scenarios` directory are designed as small Node.js scripts that spin up a headless browser and walk through a test scenario. You may
+design your own test scenarios that verify functionality specific to your CAS deployment or feature.
+
+To execute Puppeteer scenarios, run:
+
+```bash
+./puppeteer/run.sh
+```
+
+This will first attempt to build your CAS deployment, will install Puppeteer and all other needed libraries. It will then launch the CAS server,
+and upon its availability, will iterate through defined scenarios and will execute them one at a time.
+
+The following defaults are assumed:
+
+- CAS will be available at `https://localhost:8443/cas/login`.
+- The CAS overlay is prepped with an embedded server container, such as Apache Tomcat.
+
+You may of course need to make adjustments to account for your specific environment and deployment settings, URLs, etc.
+
+
+# Duct
+
+`duct` is a Gradle task to do quick smoke tests of multi-node CAS high-availability deployments. In particular, it tests correctness of ticket
+sharing between multiple individual CAS server nodes backed by distributed ticket registries such as Hazelcast, Redis, etc.
+
+This task requires CAS server nodes to **enable the CAS REST module**. It will **NOT** work without it.
+
+The task accepts the following properties:
+
+- Arbitrary number of CAS server nodes specified via the `duct.cas.X` properties.
+- URL of the service application registered with CAS specified via `duct.service`, for which tickets will be requested.
+- `duct.username` and `duct.password` to use for authentication, when requesting ticket-granting tickets.
+
+It automates the following scenario:
+
+- Authenticate and issue a service ticket on one CAS node
+- Validate this service ticket on the another node
+- Repeat (You may cancel and stop the task at any time with `Ctrl+C`)
+
+If the task succeeds, then we effectively have proven that the distributed ticket registry has been set up and deployed
+correctly and that there are no connectivity issues between CAS nodes.
+
+To run the task, you may use:
+
+```bash
+./gradlew duct
+ -Pduct.cas.1=https://node1.example.org/cas \
+ -Pduct.cas.2=https://node2.example.org/cas \
+ -Pduct.cas.3=https://node3.example.org/cas \
+ -Pduct.cas.4=https://node4.example.org/cas \
+ -Pduct.service=https://apereo.github.io \
+ -Pduct.username=casuser \
+ -Pduct.password=Mellon
+```
+
+You may also supply the following options:
+
+- `duct.debug`: Boolean flag to output debug and verbose logging.
+- `duct.duration`: Number of seconds, i.e. `30` to execute the scenario.
+- `duct.count`: Number of iterations, i.e. `5` to execute the scenario.
+
+
+# OpenRewrite
+
+[OpenRewrite](https://docs.openrewrite.org/) is a tool used by the CAS in form of a Gradle plugin
+that allows the project to upgrade in place. It works by making changes to the project structure representing
+your CAS build and printing the modified files back. Modifications are packaged together in form of upgrade
+scripts called `Recipes` that are automatically packaged and presented to the build and may be discovered via:
+
+```bash
+./gradlew --init-script openrewrite.gradle rewriteDiscover -PtargetVersion=X.Y.Z --no-configuration-cache | grep "org.apereo.cas"
+```
+
+**NOTE:** All CAS specific recipes begin with `org.apereo.cas`. The `targetVersion` must be the CAS version to which you want to upgrade.
+
+OpenRewrite recipes make minimally invasive changes to your CAS build allowing you to upgrade from one version
+to the next with minimal effort. The recipe contains *almost* everything that is required for a CAS build system to navigate
+from one version to other and automated tedious aspects of the upgrade such as finding the correct versions of CAS,
+relevant libraries and plugins as well as any possible structural changes to one's CAS build.
+
+To run, you will need to find and select the name of the recipe first. Then, you can dry-run the selected recipes and see which files would be changed in the build log.
+This does not alter your source files on disk at all. This goal can be used to preview the changes that would be made by the active recipes.
+
+```bash
+./gradlew --init-script openrewrite.gradle rewriteDryRun -PtargetVersion=X.Y.Z -DactiveRecipe=[recipe name] --no-configuration-cache
+```
+
+When you are ready, you can run the actual recipe:
+
+```bash
+./gradlew --init-script openrewrite.gradle rewriteRun -PtargetVersion=X.Y.Z -DactiveRecipe=[recipe name] --no-configuration-cache
+```
+
+This will run the selected recipes and apply the changes. This will write changes locally to your source files on disk.
+Afterward, review the changes, and when you are comfortable with the changes, commit them.
+The run goal generates warnings in the build log wherever it makes changes to source files.
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..49cbf0062
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,305 @@
+import org.apache.tools.ant.taskdefs.condition.*
+import org.gradle.internal.logging.text.*
+import org.apereo.cas.metadata.*
+import java.nio.file.*
+import java.lang.reflect.*
+import org.gradle.internal.logging.text.*
+import static org.gradle.internal.logging.text.StyledTextOutput.Style
+
+buildscript {
+ repositories {
+ if (project.privateRepoUrl) {
+ maven {
+ url project.privateRepoUrl
+ credentials {
+ username = project.privateRepoUsername
+ password = System.env.PRIVATE_REPO_TOKEN
+ }
+ }
+ }
+ mavenLocal()
+ mavenCentral()
+ gradlePluginPortal()
+ maven {
+ url 'https://oss.sonatype.org/content/repositories/snapshots'
+ mavenContent { snapshotsOnly() }
+ }
+ maven {
+ url "https://repo.spring.io/milestone"
+ mavenContent { releasesOnly() }
+ }
+ }
+ dependencies {
+ classpath "org.springframework.boot:spring-boot-gradle-plugin:${project.springBootVersion}"
+ classpath "io.freefair.gradle:maven-plugin:${project.gradleFreeFairPluginVersion}"
+ classpath "io.freefair.gradle:lombok-plugin:${project.gradleFreeFairPluginVersion}"
+ classpath "com.google.cloud.tools:jib-gradle-plugin:${project.jibVersion}"
+ classpath "com.bmuschko:gradle-docker-plugin:${project.gradleDockerPluginVersion}"
+ classpath "de.undercouch:gradle-download-task:${project.gradleDownloadTaskVersion}"
+ classpath "org.apereo.cas:cas-server-core-api-configuration-model:${project.'cas.version'}"
+ classpath "org.apereo.cas:cas-server-core-configuration-metadata-repository:${project.'cas.version'}"
+ }
+}
+
+repositories {
+ if (project.privateRepoUrl) {
+ maven {
+ url project.privateRepoUrl
+ credentials {
+ username = project.privateRepoUsername
+ password = System.env.PRIVATE_REPO_TOKEN
+ }
+ }
+ }
+ mavenLocal()
+ mavenCentral()
+ maven { url 'https://oss.sonatype.org/content/repositories/releases' }
+ maven {
+ url 'https://oss.sonatype.org/content/repositories/snapshots'
+ mavenContent { snapshotsOnly() }
+ }
+ maven {
+ url "https://repository.apache.org/content/repositories/snapshots"
+ mavenContent { snapshotsOnly() }
+ }
+ maven {
+ url 'https://build.shibboleth.net/nexus/content/repositories/releases/'
+ mavenContent { releasesOnly() }
+ }
+ maven {
+ url "https://build.shibboleth.net/nexus/content/repositories/snapshots"
+ mavenContent { snapshotsOnly() }
+ }
+ maven {
+ url "https://repo.spring.io/milestone"
+ mavenContent { releasesOnly() }
+ }
+}
+
+apply plugin: "io.freefair.war-overlay"
+apply plugin: "war"
+
+apply plugin: "org.springframework.boot"
+apply plugin: "io.freefair.lombok"
+
+
+
+apply from: rootProject.file("gradle/springboot.gradle")
+apply plugin: "com.google.cloud.tools.jib"
+apply plugin: "com.bmuschko.docker-remote-api"
+apply from: rootProject.file("gradle/tasks.gradle")
+
+def out = services.get(StyledTextOutputFactory).create("cas")
+
+configurations {
+ all {
+ resolutionStrategy {
+ cacheChangingModulesFor 0, "seconds"
+ cacheDynamicVersionsFor 0, "seconds"
+ preferProjectModules()
+ def failIfConflict = project.hasProperty("failOnVersionConflict") && Boolean.valueOf(project.getProperty("failOnVersionConflict"))
+ if (failIfConflict) {
+ failOnVersionConflict()
+ }
+
+ if (project.hasProperty("tomcatVersion")) {
+ eachDependency { DependencyResolveDetails dependency ->
+ def requested = dependency.requested
+ if (requested.group.startsWith("org.apache.tomcat") && requested.name != "jakartaee-migration") {
+ dependency.useVersion("${tomcatVersion}")
+ }
+ }
+ }
+ }
+ exclude(group: "cglib", module: "cglib")
+ exclude(group: "cglib", module: "cglib-full")
+ exclude(group: "org.slf4j", module: "slf4j-log4j12")
+ exclude(group: "org.slf4j", module: "slf4j-simple")
+ exclude(group: "org.slf4j", module: "jcl-over-slf4j")
+ exclude(group: "org.apache.logging.log4j", module: "log4j-to-slf4j")
+ }
+}
+
+war {
+ entryCompression = ZipEntryCompression.STORED
+ enabled = false
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(project.targetCompatibility)
+ def chosenJvmVendor = null
+ JvmVendorSpec.declaredFields.each { field ->
+ if (field.type == JvmVendorSpec && Modifier.isStatic(field.getModifiers())) {
+ if (field.name == project.jvmVendor?.toUpperCase()) {
+ chosenJvmVendor = field.get(null)
+ return
+ }
+ }
+ }
+ if (chosenJvmVendor != null) {
+ vendor = chosenJvmVendor
+ out.withStyle(Style.Success).println("Using ${chosenJvmVendor} as the JVM vendor for the Java toolchain")
+ } else {
+ out.withStyle(Style.Info).println("JVM vendor ${project.jvmVendor} is not recognized")
+ }
+ }
+}
+
+bootBuildImage {
+ imageName = "${project.'containerImageOrg'}/${project.'containerImageName'}:${project.version}"
+}
+
+
+['jibDockerBuild', 'jibBuildTar', 'jib'].each { taskName ->
+ if (gradle.gradleVersion >= "8.0") {
+ getTasksByName(taskName, true).each(it -> {
+ it.notCompatibleWithConfigurationCache("Jib is not compatible with configuration cache");
+ it.enabled = !gradle.startParameter.isConfigurationCacheRequested()
+ })
+ }
+}
+
+def imagePlatforms = project.dockerImagePlatform.split(",")
+def dockerUsername = providers.systemProperty("dockerUsername").getOrNull()
+def dockerPassword = providers.systemProperty("dockerPassword").getOrNull()
+def imageTagPostFix = providers.systemProperty("dockerImageTagPostfix").getOrElse("")
+
+jib {
+ if (gradle.gradleVersion >= "8.0" && gradle.startParameter.isConfigurationCacheRequested()) {
+ out.withStyle(Style.Info).println("You are seeing this message because the Gradle configuration cache is turned on")
+ out.withStyle(Style.Info).println("Running Jib tasks to produce Docker images will require the command-line option: --no-configuration-cache")
+ out.withStyle(Style.Info).println("Jib does not support the Gradle configuration cache; Please see https://github.com/GoogleContainerTools/jib/issues/3132")
+ out.withStyle(Style.Info).println("Jib tasks are disabled.")
+ }
+ from {
+ image = project.baseDockerImage
+ platforms {
+ imagePlatforms.each {
+ def given = it.split(":")
+ platform {
+ architecture = given[0]
+ os = given[1]
+ }
+ }
+ }
+ }
+ to {
+ image = "${project.'containerImageOrg'}/${project.'containerImageName'}:${project.version}"
+ /**
+ ecr-login: Amazon Elastic Container Registry (ECR)
+ gcr: Google Container Registry (GCR)
+ osxkeychain: Docker Hub
+ */
+ credHelper = "osxkeychain"
+ if (dockerUsername != null && dockerPassword != null) {
+ auth {
+ username = "${dockerUsername}"
+ password = "${dockerPassword}"
+ }
+ }
+ tags = [project.version]
+ }
+ container {
+ creationTime = "USE_CURRENT_TIMESTAMP"
+ entrypoint = ['/docker/entrypoint.sh']
+ ports = ['80', '443', '8080', '8443', '8444', '8761', '8888', '5000']
+ labels = [version:project.version, name:project.name, group:project.group, org:project.containerImageOrg]
+ workingDirectory = '/docker/cas/war'
+ }
+ extraDirectories {
+ paths {
+ path {
+ from = file('src/main/jib')
+ }
+ path {
+ from = file('etc/cas')
+ into = '/etc/cas'
+ }
+ path {
+ from = file("build/libs")
+ into = "/docker/cas/war"
+ }
+ }
+ permissions = [
+ '/docker/entrypoint.sh': '755'
+ ]
+ }
+ allowInsecureRegistries = project.allowInsecureRegistries
+}
+
+import com.bmuschko.gradle.docker.tasks.image.*
+tasks.register("casBuildDockerImage", DockerBuildImage) {
+ dependsOn("build")
+
+ def imageTag = "${project.'cas.version'}"
+ inputDir = project.projectDir
+ images.add("apereo/cas:${imageTag}${imageTagPostFix}")
+ images.add("apereo/cas:latest${imageTagPostFix}")
+ if (dockerUsername != null && dockerPassword != null) {
+ username = dockerUsername
+ password = dockerPassword
+ }
+ doLast {
+ out.withStyle(Style.Success).println("Built CAS images successfully.")
+ }
+}
+
+tasks.register("casPushDockerImage", DockerPushImage) {
+ dependsOn("casBuildDockerImage")
+
+ def imageTag = "${project.'cas.version'}"
+ images.add("apereo/cas:${imageTag}${imageTagPostFix}")
+ images.add("apereo/cas:latest${imageTagPostFix}")
+
+ if (dockerUsername != null && dockerPassword != null) {
+ username = dockerUsername
+ password = dockerPassword
+ }
+ doLast {
+ out.withStyle(Style.Success).println("Pushed CAS images successfully.")
+ }
+}
+
+
+if (project.hasProperty("appServer")) {
+ def appServer = project.findProperty('appServer') ?: ''
+ out.withStyle(Style.Success).println("Building CAS version ${project.version} with application server ${appServer}")
+} else {
+ out.withStyle(Style.Success).println("Building CAS version ${project.version} without an application server")
+}
+
+dependencies {
+ /**
+ * Do NOT modify the lines below or else you will risk breaking dependency management.
+ **/
+ implementation enforcedPlatform("org.apereo.cas:cas-server-support-bom:${project.'cas.version'}")
+ implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
+
+ /**
+ * Do NOT modify the lines below or else you will risk breaking the build.
+ **/
+ implementation "org.apereo.cas:cas-server-core-api-configuration-model"
+ implementation "org.apereo.cas:cas-server-webapp-init"
+
+ if (appServer == 'tomcat') {
+ implementation "org.apereo.cas:cas-server-webapp-init-tomcat"
+ }
+
+ developmentOnly "org.springframework.boot:spring-boot-devtools:${project.springBootVersion}"
+ // developmentOnly "org.springframework.boot:spring-boot-docker-compose:${project.springBootVersion}"
+
+ /**
+ * CAS dependencies and modules may be listed here.
+ *
+ * There is no need to specify the version number for each dependency
+ * since versions are all resolved and controlled by the dependency management
+ * plugin via the CAS bom.
+ **/
+ implementation "org.apereo.cas:cas-server-support-rest"
+
+
+
+ testImplementation "org.springframework.boot:spring-boot-starter-test"
+}
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..8f2e6ca7c
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,7 @@
+version: '3'
+services:
+ cas:
+ build: .
+ ports:
+ - "8443:8443"
+ - "8080:8080"
\ No newline at end of file
diff --git a/etc/cas/.ignore b/etc/cas/.ignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/etc/cas/config/log4j2.xml b/etc/cas/config/log4j2.xml
new file mode 100644
index 000000000..f5634a7aa
--- /dev/null
+++ b/etc/cas/config/log4j2.xml
@@ -0,0 +1,167 @@
+
+
+
+
+
+ /var/log
+ info
+ warn
+ info
+ warn
+ warn
+ warn
+ warn
+ warn
+ warn
+ warn
+ warn
+ true
+ false
+
+ casStackTraceFile
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..6e373948e
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,115 @@
+# This overlay project's version
+# For consistency and with no other effect, this is set to the CAS version itself.
+version=7.2.0-RC3
+
+# This is the CAS server version that will be deployed.
+# Versions typically are in the format of:
+# [Major].[Minor].[Patch].[Security]
+# For patch and security releases and unless explicitly stated otherwise, the below property
+# and NOTHING ELSE is the only change you usually need to make to upgrade your CAS deployment.
+cas.version=7.2.0-RC3
+
+# The Spring Boot version is very much tied to the CAS release 7.2.0-RC3
+# and must not be modified or upgraded out of band, as doing so would most likely
+# jeopardize the stability of your CAS deployment leading to unpredictable behavior.
+springBootVersion=3.4.0
+
+# The coordinates of this overlay project
+group=org.apereo.cas
+artifactId=cas-overlay
+
+# Before changing the JDK versions here, you must account for the following:
+# - Dependency Compatibility: Ensure that all libraries and frameworks you use are compatible with Java 21 and above.
+# - Environment Compatibility: Check that your deployment environments (e.g., servers, CI/CD pipelines, cloud services) support Java 21 and above.
+# Remember that this CAS build does and will only officially support Java 21. Do NOT change platform requirements unless
+# you really know what you are doing and are comfortable managing the deployment and its risks completely on your own.
+
+# This property defines the version of Java that your source code is written in.
+# It ensures that your code is compatible with the specified version of the Java language.
+# Gradle will expect your source code to be compatible with JDK 21.
+sourceCompatibility=21
+
+# This property specifies the version of the Java runtime that the compiled bytecode should be compatible with.
+# It ensures that the bytecode generated by the compiler will run on JDK 21.
+targetCompatibility=21
+
+# This property controls the JVM vendor that is used by Gradle toolchains.
+# You may want to build CAS using a Java version that is not supported for running Gradle
+# by setting this property to the vendor of the JDK you want to use.
+# If Gradle can’t find a locally available toolchain that matches the requirements
+# of the build, it can automatically download one.
+# Options include: AMAZON, ADOPTIUM, JETBRAINS, MICROSOFT, ORACLE, SAP, BELLSOFT, etc.
+# Setting this to a blank or invalid value will force Gradle to use the JDK installation on the build machine.
+jvmVendor=AMAZON
+
+# This plugin controls how JDK distributions required by the Grtadle toolchain
+# are discovered, and downloaded when necessary.
+# Note that auto-provisioning of a JDK distribution only kicks in when auto-detection fails
+# to find a matching JDK, and auto-provisioning can only download new JDKs and is in no way
+# involved in updating any of the already installed ones.
+gradleFoojayPluginVersion=0.8.0
+
+gradleFreeFairPluginVersion=8.11
+
+# Used to build Docker images
+jibVersion=3.4.4
+gradleDockerPluginVersion=9.4.0
+
+# Specify the coordinates of the container image to build
+containerImageOrg=apereo
+containerImageName=cas
+
+baseDockerImage=azul/zulu-openjdk:21
+allowInsecureRegistries=false
+
+# Multiple platforms may be specified, separated by a comma i.e amd64:linux,arm64:linux
+dockerImagePlatform=amd64:linux
+
+# Include a launch script for executable WAR artifact
+# Setting this to true allows the final web application
+# to be fully executable on its own.
+executable=true
+
+
+# Use -tomcat, -jetty, -undertow for deployment to control the embedded server container
+# that will be used to deploy and manage your CAS deployment.
+# You should set this to blank if you want to deploy to an external container.
+# and want to set up, download and manage the container (i.e. Apache Tomcat) yourself.
+appServer=-tomcat
+
+# If you are using an embedded Apache Tomcat container to deploy and run CAS,
+# and need to override the Apache Tomcat version, uncomment the property below
+# and specify the the Apache Tomcat version, i.e. 10.1.34.
+# While enabled, this will override any and all upstream changes to
+# Apache Tomcat dependency management and you will be directly responsible to make
+# adjustments and upgrades as necessary. Use with caution, favor less work.
+# tomcatVersion=10.1.34
+
+# Settings to generate a keystore
+# used by the build to assist with creating
+# self-signed certificates for TLS
+certDir=/etc/cas
+serverKeystore=thekeystore
+exportedServerCert=cas.crt
+storeType=PKCS12
+
+# Location of the downloaded CAS Shell JAR
+shellDir=build/libs
+ivyVersion=2.5.2
+gradleDownloadTaskVersion=4.1.1
+
+# Include private repository
+# override these in user properties or pass in values from env on command line
+privateRepoUrl=
+privateRepoUsername=
+
+# Gradle build settings
+# Do NOT modify unless you know what you're doing!
+org.gradle.configureondemand=true
+org.gradle.caching=true
+org.gradle.parallel=true
+org.gradle.jvmargs=-Xms1024m -Xmx4048m -XX:TieredStopAtLevel=1
+org.gradle.unsafe.configuration-cache=false
+org.gradle.unsafe.configuration-cache-problems=warn
+systemProp.org.gradle.internal.http.connectionTimeout=60000
+systemProp.org.gradle.internal.http.requestTimeout=120000
diff --git a/gradle/springboot.gradle b/gradle/springboot.gradle
new file mode 100644
index 000000000..99b3db7e2
--- /dev/null
+++ b/gradle/springboot.gradle
@@ -0,0 +1,135 @@
+apply plugin: "java"
+
+sourceSets {
+ bootRunSources {
+ resources {
+ srcDirs new File("//etc/cas/templates/"), new File("${project.getProjectDir()}/src/main/resources/")
+ }
+ }
+}
+
+configurations {
+ bootRunConfig {
+ extendsFrom compileClasspath
+
+ exclude(group: "org.springframework.boot", module: "spring-boot-starter-logging")
+ exclude(group: "ch.qos.logback", module: "logback-core")
+ exclude(group: "ch.qos.logback", module: "logback-classic")
+ }
+}
+
+dependencies {
+ bootRunConfig "org.apereo.cas:cas-server-core"
+ bootRunConfig "org.apereo.cas:cas-server-core-logging"
+ bootRunConfig "org.apereo.cas:cas-server-core-web"
+ bootRunConfig "org.apereo.cas:cas-server-core-webflow"
+ bootRunConfig "org.apereo.cas:cas-server-core-cookie"
+ bootRunConfig "org.apereo.cas:cas-server-core-logout"
+ bootRunConfig "org.apereo.cas:cas-server-core-authentication"
+ bootRunConfig "org.apereo.cas:cas-server-core-validation"
+ bootRunConfig "org.apereo.cas:cas-server-core-audit"
+ bootRunConfig "org.apereo.cas:cas-server-core-tickets"
+ bootRunConfig "org.apereo.cas:cas-server-core-services"
+ bootRunConfig "org.apereo.cas:cas-server-core-util"
+
+ bootRunConfig "org.apereo.cas:cas-server-support-webconfig"
+ bootRunConfig "org.apereo.cas:cas-server-support-thymeleaf"
+ bootRunConfig "org.apereo.cas:cas-server-support-validation"
+ bootRunConfig "org.apereo.cas:cas-server-support-person-directory"
+ bootRunConfig "org.apereo.cas:cas-server-webapp-resources"
+ bootRunConfig "org.apereo.cas:cas-server-webapp-init"
+ bootRunConfig "org.apereo.cas:cas-server-webapp-tomcat"
+ bootRunConfig "org.apereo.cas:cas-server-webapp-init-tomcat"
+
+ bootRunConfig "org.springframework.cloud:spring-cloud-starter-bootstrap"
+ bootRunConfig "org.springframework.boot:spring-boot-devtools"
+}
+
+bootRun {
+ classpath = configurations.bootRunConfig + sourceSets.main.compileClasspath + sourceSets.main.runtimeClasspath
+ sourceResources sourceSets.bootRunSources
+ doFirst {
+ systemProperties = System.properties
+ }
+
+ def list = []
+ list.add("-XX:TieredStopAtLevel=1")
+ list.add("-Xverify:none")
+ list.add("--add-modules")
+ list.add("java.se")
+ list.add("--add-exports")
+ list.add("java.base/jdk.internal.ref=ALL-UNNAMED")
+ list.add("--add-opens")
+ list.add("java.base/java.lang=ALL-UNNAMED")
+ list.add("--add-opens")
+ list.add("java.base/java.nio=ALL-UNNAMED")
+ list.add("--add-opens")
+ list.add("java.base/sun.nio.ch=ALL-UNNAMED")
+ list.add("--add-opens")
+ list.add("java.management/sun.management=ALL-UNNAMED")
+ list.add("--add-opens")
+ list.add("jdk.management/com.sun.management.internal=ALL-UNNAMED")
+ list.add("-Xrunjdwp:transport=dt_socket,address=5000,server=y,suspend=n")
+
+ jvmArgs = list
+
+ def appArgList = ["--spring.thymeleaf.cache=false"]
+ args = appArgList
+}
+
+springBoot {
+ mainClass = "org.apereo.cas.web.CasWebApplication"
+
+}
+
+
+bootWar {
+ def executable = project.hasProperty("executable") && Boolean.valueOf(project.getProperty("executable"))
+ if (executable) {
+ logger.info "Including launch script for executable WAR artifact"
+ launchScript()
+ } else {
+ logger.info "WAR artifact is not marked as an executable"
+ }
+
+ archiveFileName = "cas.war"
+ archiveBaseName = "cas"
+
+ entryCompression = ZipEntryCompression.STORED
+
+ /*
+ attachClasses = true
+ classesClassifier = 'classes'
+ archiveClasses = true
+ */
+
+
+ overlays {
+ /*
+ https://docs.freefair.io/gradle-plugins/current/reference/#_io_freefair_war_overlay
+ Note: The "excludes" property is only for files in the war dependency.
+ If a jar is excluded from the war, it could be brought back into the final war as a dependency
+ of non-war dependencies. Those should be excluded via normal gradle dependency exclusions.
+ */
+ cas {
+ from "org.apereo.cas:cas-server-webapp${project.findProperty('appServer') ?: ''}:${project.'cas.version'}@war"
+
+
+ provided = false
+
+ def excludeArtifacts = ["WEB-INF/lib/servlet-api-2*.jar"]
+ if (project.hasProperty("tomcatVersion")) {
+ excludes += ["WEB-INF/lib/tomcat-*.jar"]
+ }
+ excludes = excludeArtifacts
+
+ /*
+ excludes = ["WEB-INF/lib/somejar-1.0*"]
+ enableCompilation = true
+ includes = ["*.xyz"]
+ targetPath = "sub-path/bar"
+ skip = false
+ */
+ }
+ }
+}
diff --git a/gradle/tasks.gradle b/gradle/tasks.gradle
new file mode 100644
index 000000000..ad85cacde
--- /dev/null
+++ b/gradle/tasks.gradle
@@ -0,0 +1,520 @@
+import static org.gradle.internal.logging.text.StyledTextOutput.Style
+
+import org.apereo.cas.metadata.*
+import org.gradle.internal.logging.text.*
+
+import groovy.json.*
+import groovy.time.*
+
+import java.nio.file.*
+import java.util.*
+import java.security.*
+
+buildscript {
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ gradlePluginPortal()
+ maven {
+ url 'https://oss.sonatype.org/content/repositories/snapshots'
+ mavenContent { snapshotsOnly() }
+ }
+ maven {
+ url "https://repo.spring.io/milestone"
+ mavenContent { releasesOnly() }
+ }
+ }
+ dependencies {
+ classpath "org.apache.ivy:ivy:${project.ivyVersion}"
+ classpath "org.apereo.cas:cas-server-core-configuration-metadata-repository:${project.'cas.version'}"
+ }
+}
+apply plugin: "de.undercouch.download"
+
+task run(group: "build", description: "Run the CAS web application in embedded container mode") {
+ dependsOn 'build'
+ doLast {
+ def casRunArgs = Arrays.asList("-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" "))
+ project.javaexec {
+ jvmArgs = casRunArgs
+ classpath = project.files("build/libs/cas.war")
+ systemProperties = System.properties
+ logger.info "Started ${commandLine}"
+ }
+ }
+}
+
+task setExecutable(group: "CAS", description: "Configure the project to run in executable mode") {
+ doFirst {
+ project.setProperty("executable", "true")
+ logger.info "Configuring the project as executable"
+ }
+}
+
+task executable(type: Exec, group: "CAS", description: "Run the CAS web application in standalone executable mode") {
+ dependsOn setExecutable, 'build'
+ doFirst {
+ workingDir "."
+ if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
+ commandLine "chmod", "+x", bootWar.archivePath
+ }
+ logger.info "Running ${bootWar.archivePath}"
+ commandLine bootWar.archivePath
+ }
+}
+
+
+task debug(group: "CAS", description: "Debug the CAS web application in embedded mode on port 5005") {
+ dependsOn 'build'
+ doLast {
+ logger.info "Debugging process is started in a suspended state, listening on port 5005."
+ def casArgs = Arrays.asList("-Xmx2048M".split(" "))
+ project.javaexec {
+ jvmArgs = casArgs
+ debug = true
+ classpath = project.files("build/libs/cas.war")
+ systemProperties = System.properties
+ logger.info "Started ${commandLine}"
+ }
+ }
+}
+
+task showConfiguration(group: "CAS", description: "Show configurations for each dependency, etc") {
+ doLast() {
+ def cfg = project.hasProperty("configuration") ? project.property("configuration") : "compile"
+ configurations.getByName(cfg).each { println it }
+ }
+}
+
+task allDependenciesInsight(group: "build", type: DependencyInsightReportTask, description: "Produce insight information for all dependencies") {}
+
+task allDependencies(group: "build", type: DependencyReportTask, description: "Display a graph of all project dependencies") {}
+
+task casVersion(group: "CAS", description: "Display the current CAS version") {
+ doFirst {
+ def verbose = project.hasProperty("verbose") && Boolean.valueOf(project.getProperty("verbose"))
+ if (verbose) {
+ def out = services.get(StyledTextOutputFactory).create("CAS")
+ println "******************************************************************"
+ out.withStyle(Style.Info).println "Apereo CAS ${project.version}"
+ out.withStyle(Style.Description).println "Enterprise Single SignOn for all earthlings and beyond"
+ out.withStyle(Style.SuccessHeader).println "- GitHub: "
+ out.withStyle(Style.Success).println "https://github.com/apereo/cas"
+ out.withStyle(Style.SuccessHeader).println "- Docs: "
+ out.withStyle(Style.Success).println "https://apereo.github.io/cas"
+ out.withStyle(Style.SuccessHeader).println "- Blog: "
+ out.withStyle(Style.Success).println "https://apereo.github.io"
+ println "******************************************************************"
+ } else {
+ println project.version
+ }
+ }
+}
+
+task springBootVersion(description: "Display current Spring Boot version") {
+ doLast {
+ println rootProject.springBootVersion
+ }
+}
+
+task zip(type: Zip) {
+ from projectDir
+ exclude '**/.idea/**', '.gradle', 'tmp', '.git', '**/build/**', '**/bin/**', '**/out/**', '**/.settings/**'
+ destinationDirectory = buildDir
+ archiveFileName = "${project.name}.zip"
+ def zipFile = new File("${buildDir}/${archiveFileName}")
+ doLast {
+ if (zipFile.exists()) {
+ println "Zip archive is available at ${zipFile.absolutePath}"
+ }
+ }
+}
+
+task createKeystore(group: "CAS", description: "Create CAS keystore") {
+ def dn = "CN=cas.example.org,OU=Example,OU=Org,C=US"
+ if (project.hasProperty("certificateDn")) {
+ dn = project.getProperty("certificateDn")
+ }
+ def subjectAltName = "dns:example.org,dns:localhost,ip:127.0.0.1"
+ if (project.hasProperty("certificateSubAltName")) {
+ subjectAltName = project.getProperty("certificateSubAltName")
+ }
+
+ doFirst {
+ def certDir = project.getProperty("certDir")
+ def serverKeyStore = project.getProperty("serverKeystore")
+ def exportedServerCert = project.getProperty("exportedServerCert")
+ def storeType = project.getProperty("storeType")
+ def keystorePath = "$certDir/$serverKeyStore"
+ def serverCert = "$certDir/$exportedServerCert"
+
+ mkdir certDir
+ // this will fail if thekeystore exists and has cert with cas alias already (so delete if you want to recreate)
+ logger.info "Generating keystore for CAS with DN ${dn}"
+ exec {
+ workingDir "."
+ commandLine "keytool", "-genkeypair", "-alias", "cas",
+ "-keyalg", "RSA",
+ "-keypass", "changeit", "-storepass", "changeit",
+ "-keystore", keystorePath,
+ "-dname", dn, "-ext", "SAN=${subjectAltName}",
+ "-storetype", storeType
+ }
+ logger.info "Exporting cert from keystore..."
+ exec {
+ workingDir "."
+ commandLine "keytool", "-exportcert", "-alias", "cas",
+ "-storepass", "changeit", "-keystore", keystorePath,
+ "-file", serverCert
+ }
+ logger.info "Import $serverCert into your Java truststore (\$JAVA_HOME/lib/security/cacerts)"
+ }
+}
+
+task unzipWAR(type: Copy, group: "CAS", description: "Explodes the CAS web application archive") {
+ dependsOn 'build'
+ def destination = "${buildDir}/app"
+
+ from zipTree("build/libs/cas.war")
+ into "${destination}"
+ doLast {
+ println "Unzipped WAR into ${destination}"
+ }
+}
+
+task verifyRequiredJavaVersion {
+ def currentVersion = org.gradle.api.JavaVersion.current()
+ logger.info "Checking current Java version ${currentVersion} for required Java version ${project.targetCompatibility}"
+ def targetVersion = JavaVersion.toVersion(project.targetCompatibility)
+ if (!currentVersion.isCompatibleWith(targetVersion)) {
+ logger.warn("Careful: Current Java version ${currentVersion} does not match required Java version ${project.targetCompatibility}")
+ }
+}
+
+task copyCasConfiguration(type: Copy, group: "CAS",
+ description: "Copy the CAS configuration from this project to /etc/cas/config") {
+ from "etc/cas/config"
+ into new File('/etc/cas/config').absolutePath
+ doFirst {
+ new File('/etc/cas/config').mkdirs()
+ }
+}
+
+
+def explodedDir = "${buildDir}/app"
+def explodedResourcesDir = "${buildDir}/cas-resources"
+
+def resourcesJarName = "cas-server-webapp-resources"
+def templateViewsJarName = "cas-server-support-thymeleaf"
+
+task unzip(type: Copy, group: "CAS", description: "Explodes the CAS archive and resources jar from the CAS web application archive") {
+ dependsOn unzipWAR
+ from zipTree("${explodedDir}/WEB-INF/lib/${templateViewsJarName}-${project.'cas.version'}.jar")
+ into explodedResourcesDir
+
+ from zipTree("${explodedDir}/WEB-INF/lib/${resourcesJarName}-${project.'cas.version'}.jar")
+ into explodedResourcesDir
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ doLast {
+ println "Exploded WAR resources into ${explodedResourcesDir}"
+ }
+}
+
+task downloadShell(group: "Shell", description: "Download CAS shell jar from snapshot or release maven repo", type: Download) {
+ def shellDir = project.providers.gradleProperty("shellDir").get()
+ def casVersion = project.providers.gradleProperty("cas.version").get()
+ def downloadFile
+ if (casVersion.contains("-SNAPSHOT")) {
+ def snapshotDir = "https://oss.sonatype.org/content/repositories/snapshots/org/apereo/cas/cas-server-support-shell/${casVersion}/"
+ def files = new org.apache.ivy.util.url.ApacheURLLister().listFiles(new URL(snapshotDir))
+ files = files.sort { it.path }
+ files.each {
+ if (it.path.endsWith(".jar")) {
+ downloadFile = it
+ }
+ }
+ } else {
+ downloadFile = "https://repo1.maven.org/maven2/org/apereo/cas/cas-server-support-shell/${casVersion}/cas-server-support-shell-${casVersion}.jar"
+ }
+ new File("${shellDir}").mkdir()
+ logger.info "Downloading file: ${downloadFile}"
+ src downloadFile
+ dest new File("${shellDir}", "cas-server-support-shell-${casVersion}.jar")
+ overwrite false
+}
+
+task runShell(group: "Shell", description: "Run the CAS shell") {
+ dependsOn downloadShell
+ def casVersion = project.providers.gradleProperty("cas.version").get()
+ doLast {
+ println "Run the following command to launch the shell:\n\tjava -jar ${project.shellDir}/cas-server-support-shell-${casVersion}.jar"
+ }
+}
+
+task debugShell(group: "Shell", description: "Run the CAS shell with debug options, wait for debugger on port 5005") {
+ dependsOn downloadShell
+ def casVersion = project.providers.gradleProperty("cas.version").get()
+ doLast {
+ println """
+ Run the following command to launch the shell:\n\t
+ java -Xrunjdwp:transport=dt_socket,address=5000,server=y,suspend=y -jar ${project.shellDir}/cas-server-support-shell-${casVersion}.jar
+ """
+ }
+}
+
+task listTemplateViews(group: "CAS", description: "List all CAS views") {
+ dependsOn unzip
+
+ def templateViews = fileTree(explodedResourcesDir).matching {
+ include "**/*.html"
+ }
+ .collect {
+ return it.path.replace(explodedResourcesDir, "")
+ }
+ .toSorted()
+
+ doFirst {
+ templateViews.each { println it }
+ }
+}
+
+task getResource(group: "CAS", description: "Fetch a CAS resource and move it into the overlay") {
+ dependsOn unzip
+
+ def resourceName = project.providers.gradleProperty("resourceName").getOrNull()
+ def resourcesDirectory = fileTree(explodedResourcesDir)
+ def projectDirectory = projectDir
+
+ doFirst {
+ def results = resourcesDirectory.matching {
+ include "**/${resourceName}.*"
+ include "**/${resourceName}"
+ }
+ if (results.isEmpty()) {
+ println "No resources could be found matching ${resourceName}"
+ return
+ }
+ if (results.size() > 1) {
+ println "Multiple resources found matching ${resourceName}:\n"
+ results.each {
+ println "\t-" + it.path.replace(explodedResourcesDir, "")
+ }
+ println "\nNarrow down your search criteria and try again."
+ return
+ }
+
+ def fromFile = explodedResourcesDir
+ def resourcesDir = "src/main/resources"
+ new File(resourcesDir).mkdir()
+
+ def resourceFile = results[0].canonicalPath
+ def toResourceFile = new File("${projectDirectory}", resourceFile.replace(fromFile, resourcesDir))
+ toResourceFile.getParentFile().mkdirs()
+
+ Files.copy(Paths.get(resourceFile), Paths.get(toResourceFile.absolutePath), StandardCopyOption.REPLACE_EXISTING)
+ println "Copied file ${resourceFile} to ${toResourceFile}"
+ }
+}
+
+task createTheme(group: "CAS", description: "Create theme directory structure in the overlay") {
+ def theme = project.providers.gradleProperty("theme").getOrNull()
+
+ doFirst {
+ def builder = new FileTreeBuilder()
+ new File("src/main/resources/${theme}.properties").delete()
+
+ builder.src {
+ main {
+ resources {
+ "static" {
+ themes {
+ "${theme}" {
+ css {
+ 'cas.css'('')
+ }
+ js {
+ 'cas.js'('')
+ }
+ images {
+ '.ignore'('')
+ }
+ }
+ }
+ }
+
+ templates {
+ "${theme}" {
+ fragments {
+
+ }
+ }
+ }
+
+ "${theme}.properties"("""cas.standard.css.file=/themes/${theme}/css/cas.css
+cas.standard.js.file=/themes/${theme}/js/cas.js
+ """)
+ }
+ }
+ }
+ }
+}
+
+def skipValidation = project.hasProperty("validate") && project.property("validate").equals("false")
+if (!skipValidation) {
+ task validateConfiguration(type: Copy, group: "CAS",
+ description: "Validate CAS configuration") {
+ def file = new File("${projectDir}/src/main/resources/application.properties")
+ if (file.exists()) {
+ throw new GradleException("This overlay project is overriding a CAS-supplied configuration file at ${file.path}. "
+ + "Overriding this file will disable all default CAS settings that are provided to the overlay, and "
+ + "generally has unintended side-effects. It's best to move your configuration inside an application.yml "
+ + "file, if you intend to keep the configuration bundled with the CAS web application. \n\nTo disable this "
+ + "validation step, run the build with -Pvalidate=false.");
+ }
+ }
+ processResources.dependsOn(validateConfiguration)
+}
+
+task duct(group: "CAS", description: "Test ticket registry functionality via the CAS REST API") {
+ def service = project.findProperty("duct.service") ?: "https://apereo.github.io"
+ def casServerNodes = providers.gradlePropertiesPrefixedBy("duct.cas").get()
+ def username = project.findProperty("duct.username") ?: "casuser"
+ def password = project.findProperty("duct.password") ?: "Mellon"
+ def debug = Boolean.parseBoolean(project.findProperty("duct.debug") ?: "false")
+ def duration = Long.parseLong(project.findProperty("duct.duration") ?: "-1")
+ def count = Long.parseLong(project.findProperty("duct.count") ?: "-1")
+
+ doLast {
+ def out = services.get(StyledTextOutputFactory).create("cas")
+
+ def getCasServerNode = {
+ def casServerNodesArray = casServerNodes.values().toArray()
+ return casServerNodesArray[new SecureRandom().nextInt(casServerNodesArray.length)] as String
+ }
+
+ def startTime = new Date()
+ def keepGoing = true
+ def executionCount = 0
+
+ while(keepGoing) {
+ executionCount++
+
+ def casServerPrefix1 = getCasServerNode()
+ def casServerPrefix2 = getCasServerNode()
+
+ if (casServerNodes.size() >= 2) {
+ while (casServerPrefix1.equals(casServerPrefix2)) {
+ casServerPrefix2 = getCasServerNode()
+ }
+ }
+
+ if (debug) {
+ out.withStyle(Style.Normal).println("CAS Server 1: ${casServerPrefix1}")
+ out.withStyle(Style.Normal).println("CAS Server 2: ${casServerPrefix2}")
+ out.withStyle(Style.Normal).println("Fetching ticket-granting ticket @ ${casServerPrefix1} for ${username}...")
+ }
+ def connection = new URL("${casServerPrefix1}/v1/tickets").openConnection()
+ connection.setRequestMethod("POST")
+ connection.setDoOutput(true)
+ connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
+ connection.getOutputStream().write("username=${username}&password=${password}".getBytes("UTF-8"))
+ def rc = connection.getResponseCode()
+
+ if (rc == 201) {
+ def tgt = connection.getHeaderFields().get("Location").get(0).find('TGT-.*')
+
+ if (debug) {
+ out.withStyle(Style.Normal).println("Received ticket-granting ticket ${tgt} @ ${casServerPrefix2} for ${username}...")
+ out.withStyle(Style.Normal).println("Fetching service ticket @ ${casServerPrefix2} for ${tgt} and service ${service}...")
+ }
+ connection = new URL("${casServerPrefix2}/v1/tickets/${tgt}").openConnection()
+ connection.setRequestMethod("POST")
+ connection.setDoOutput(true)
+ connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
+ connection.getOutputStream().write("service=${service}".getBytes("UTF-8"))
+ rc = connection.getResponseCode()
+
+ if (rc == 200) {
+ def st = connection.getInputStream().getText()
+ if (debug) {
+ out.withStyle(Style.Normal).println("Received service ticket ${st} @ ${casServerPrefix1} for ${service}...")
+ out.withStyle(Style.Normal).println("Validating service ticket ${st} @ ${casServerPrefix1} for service ${service}...")
+ }
+ connection = new URL("${casServerPrefix1}/p3/serviceValidate?service=${service}&ticket=${st}&format=json").openConnection()
+ connection.setRequestMethod("GET")
+ connection.setDoOutput(true)
+ connection.setRequestProperty("Content-Type", "application/json")
+ rc = connection.getResponseCode()
+
+ if (rc == 200) {
+ def serverResponse = connection.getInputStream().getText()
+ def response = new JsonSlurper().parseText(serverResponse)
+
+ if (response.serviceResponse["authenticationSuccess"] != null) {
+ out.withStyle(Style.Success).println("Service ticket ${st} is successfully validated @ ${casServerPrefix1}")
+ } else {
+ out.withStyle(Style.Failure).println("Service ticket ${st} cannot be validated @ ${casServerPrefix1} for ${tgt}")
+ if (debug) {
+ out.withStyle(Style.Failure).println(serverResponse)
+ }
+ }
+ } else {
+ out.withStyle(Style.Failure).println("${rc}: Unable to validate service ticket ${st} @ ${casServerPrefix1} for ${tgt}")
+ }
+ } else {
+ out.withStyle(Style.Failure).println("${rc}: Unable to fetch service ticket @ ${casServerPrefix2} for ${tgt}")
+ }
+ } else {
+ out.withStyle(Style.Failure).println("${rc}: Unable to fetch ticket-granting ticket @ ${casServerPrefix1}")
+ }
+
+ if (keepGoing && duration > 0) {
+ def executionDuration = TimeCategory.minus(new Date(), startTime)
+ keepGoing = executionDuration.getSeconds() < duration
+ }
+ if (keepGoing) {
+ keepGoing = executionCount < count
+ }
+ Thread.sleep(250)
+ }
+ }
+}
+
+task exportConfigMetadata(group: "CAS", description: "Export collection of CAS properties") {
+ def file = new File(project.rootDir, 'config-metadata.properties')
+ def queryType = ConfigurationMetadataCatalogQuery.QueryTypes.CAS
+ if (project.hasProperty("queryType")) {
+ queryType = ConfigurationMetadataCatalogQuery.QueryTypes.valueOf(project.findProperty("queryType"))
+ }
+ doLast {
+ file.withWriter('utf-8') { writer ->
+ def props = CasConfigurationMetadataCatalog.query(
+ ConfigurationMetadataCatalogQuery.builder()
+ .queryType(queryType)
+ .build())
+ .properties()
+ props.each { property ->
+ writer.writeLine("# Type: ${property.type}");
+ writer.writeLine("# Module: ${property.module}")
+ writer.writeLine("# Owner: ${property.owner}")
+ if (property.deprecationLevel != null) {
+ writer.writeLine("# This setting is deprecated with a severity level of ${property.deprecationLevel}.")
+ if (property.deprecationReason != null) {
+ writer.writeLine("# because ${property.deprecationReason}")
+ }
+ if (property.deprecationReason != null) {
+ writer.writeLine("# Replace with: ${property.deprecationReason}")
+ }
+ }
+ writer.writeLine("#")
+ def description = property.description.replace("\n", "\n# ").replace("\r", "")
+ description = org.apache.commons.text.WordUtils.wrap(description, 70, "\n# ", true)
+ writer.writeLine("# ${description}")
+ writer.writeLine("#")
+ writer.writeLine("# ${property.name}: ${property.defaultValue}")
+ writer.writeLine("")
+ }
+ }
+ println "Configuration metadata is available at ${file.absolutePath}"
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..943f0cbfa
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..4eaec4670
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..65dcd68d6
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,244 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100755
index 000000000..93e3f59f1
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/helm/README.md b/helm/README.md
new file mode 100644
index 000000000..3ed398bd2
--- /dev/null
+++ b/helm/README.md
@@ -0,0 +1,113 @@
+## Helm Chart for CAS
+
+The current helm chart for cas-server demonstrates standing up CAS.
+The chart functionality will grow over time, hopefully with contributions from real world deployments.
+Eventually it might be nice to support a config-server.
+The chart supports mapping in arbitrary volumes and cas config can be specified in values files.
+The config could be in cloud config rather than kubernetes config maps, the service registry
+could be in a database, git, or a simple json registry in a kubernetes persistent volume. The ticket registry could use a standard helm chart for redis,
+postgresql, or mongo, etc.
+Currently the chart is attempting to use SSL between ingress controller and the CAS servers.
+This is probably overkill and involves all the pain that comes with SSL (e.g. trust & hostname verification).
+This chart uses stateful set for CAS rather than a deployment and this may change in the future.
+
+#### Warning: semver versioning will not be employed until published to a repository.
+
+### Install Kubernetes (Docker for Windows/Mac, Minikube, K3S, Rancher, etc)
+
+ - [Docker Desktop](https://www.docker.com/products/docker-desktop)
+
+ - [Minikube](https://minikube.sigs.k8s.io/docs/start/)
+
+ - [k3s](https://k3s.io/) - Works on linux, very light-weight and easy to install for development
+ ```shell script
+ curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --disable traefik" sh
+ # the following export is for helm
+ export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
+ ./gradlew clean build jibBuildTar --refresh-dependencies
+ k3s ctr images import build/jib-image.tar
+ k3s ctr images ls | grep cas
+ ./gradlew createKeystore
+ cd helm
+ # create secret for tomcat
+ kubectl create secret generic cas-server-keystore --from-file=thekeystore=/etc/cas/thekeystore
+ # create secret for ingress controller to use with CAS ingress (nginx-ingress will use default if you don't create)
+ ./create-ingress-tls.sh
+ # install cas-server helm chart
+ helm upgrade --install cas-server ./cas-server
+ ```
+
+### Install Helm and Kubectl
+
+Helm v3 and Kubectl are just single binary programs. Kubectl may come with your kubernetes
+installation, but you can download both of programs and put them in your path.
+ - Install [Helm](https://helm.sh/docs/intro/install/)
+ - Install [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
+
+### Install ingress controller
+
+CAS helm chart only tested with Kubernetes ingress-nginx, feel free to add support for other ingress controllers.
+
+[Kubernetes Nginx Ingress Installation Guide](https://kubernetes.github.io/ingress-nginx/deploy/)
+
+### Create secret containing keystore
+
+Assuming you have run `./gradlew createKeystore` or put you server keystore in `/etc/cas/thekeystore`,
+run the following to create a secret containing the keystore:
+```shell script
+kubectl create secret generic cas-server-keystore --from-file=thekeystore=/etc/cas/thekeystore
+```
+
+### Install CAS Server helm chart
+
+Helm charts consist of templates which are combined with values from one or more values files
+(and command line set arguments) to produce kubernetes yaml. The templates folder contains a default
+values.yaml that is used by default but additional values files can be specified on the command line.
+The following examples use the `default` namespace but `--namespace cas` can be added to any resources
+created by the helm command to use the specified kubernetes namespace.
+```
+# delete cas-server helm chart install
+helm delete cas-server
+# install cas-server chart
+helm install cas-server ./cas-server
+# install or update cas-server
+helm upgrade --install cas-server ./cas-server
+# use local values file to override defaults
+helm upgrade --install cas-server --values values-local.yaml ./cas-server
+# see kubernetes yaml without installing
+helm upgrade --install cas-server --values values-local.yaml ./cas-server --dry-run --debug
+# sometimes dry-run fails b/c yaml can't convert to json so use template instead to see problem
+helm template cas-server --values values-local.yaml ./cas-server --debug
+```
+
+### Useful `kubectl` Commands
+
+```
+# tail the console logs
+kubectl logs cas-server-0 -f
+# exec into container
+kubectl exec -it cas-server-0 sh
+# bounce CAS pod
+kubectl delete pod cas-server-0
+```
+
+### Browse to CAS
+
+Make sure you have host entries for whatever host is listed in values file for this entry:
+```
+ingress:
+ hosts:
+ - host: cas.example.org
+ paths:
+ - "/cas"
+ tls:
+ - secretName: cas-server-ingress-tls
+ hosts:
+ - cas.example.org
+```
+
+```
+# host entry
+127.0.0.1 cas.example.org
+```
+Browse to `https://cas.example.org/cas/login`
diff --git a/helm/cas-server/.helmignore b/helm/cas-server/.helmignore
new file mode 100644
index 000000000..0e8a0eb36
--- /dev/null
+++ b/helm/cas-server/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/helm/cas-server/Chart.yaml b/helm/cas-server/Chart.yaml
new file mode 100644
index 000000000..c999c35ee
--- /dev/null
+++ b/helm/cas-server/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: cas-server
+description: A Helm chart for CAS SSO Server
+icon: "https://apereo.github.io/cas/images/cas_logo.png"
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+appVersion: 'latest'
diff --git a/helm/cas-server/templates/NOTES.txt b/helm/cas-server/templates/NOTES.txt
new file mode 100644
index 000000000..37db28b75
--- /dev/null
+++ b/helm/cas-server/templates/NOTES.txt
@@ -0,0 +1,23 @@
+1. Get the application URL by running these commands:
+{{- if .Values.cas.ingress.enabled }}
+{{- range $host := .Values.cas.ingress.hosts }}
+ {{- range .paths }}
+ curl -k -v http{{ if $.Values.cas.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}/login
+ {{- end }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+ export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "cas-server.fullname" . }})
+ export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+ echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+ NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+ You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "cas-server.fullname" . }}'
+ export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "cas-server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
+ echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+ export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "cas-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+ echo "Visit http://127.0.0.1:8080 to use your application"
+ kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80
+{{- end }}
+
+Kubernetes Version: {{ .Capabilities.KubeVersion.Version }}
diff --git a/helm/cas-server/templates/_helpers.tpl b/helm/cas-server/templates/_helpers.tpl
new file mode 100644
index 000000000..06e78af90
--- /dev/null
+++ b/helm/cas-server/templates/_helpers.tpl
@@ -0,0 +1,128 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "cas-server.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "cas-server.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "cas-server.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "cas-server.labels" -}}
+helm.sh/chart: {{ include "cas-server.chart" . }}
+{{ include "cas-server.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "cas-server.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "cas-server.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "cas-server.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "cas-server.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
+
+{{/*
+Return the proper cas-server image name
+*/}}
+{{- define "cas-server.imageName" -}}
+{{ include "common.images.image" (dict "imageRoot" .Values.image "global" .Values.global) }}
+{{- end -}}
+
+
+{{/*
+Return the proper image name (for the init container volume-permissions image)
+*/}}
+{{- define "cas-server.volumePermissions.image" -}}
+{{ include "common.images.image" (dict "imageRoot" .Values.volumePermissions.image "global" .Values.global) }}
+{{- end -}}
+
+{{/*
+Return the proper image name
+{{ include "common.images.image" ( dict "imageRoot" .Values.path.to.the.image "global" $) }}
+*/}}
+{{- define "common.images.image" -}}
+{{- $registryName := .imageRoot.registry -}}
+{{- $repositoryName := .imageRoot.repository -}}
+{{- $tag := default "latest" .imageRoot.tag | toString -}}
+{{- if .global }}
+ {{- if .global.imageRegistry }}
+ {{- $registryName = .global.imageRegistry -}}
+ {{- end -}}
+{{- end -}}
+{{- if ne $registryName "" }}
+ {{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}
+{{- else -}}
+ {{- printf "%s:%s" $repositoryName $tag -}}
+{{- end -}}
+{{- end -}}
+
+
+{{/*
+Return log directory volume
+*/}}
+{{- define "cas-server.logdir" -}}
+{{- if .Values.logdir.hostPath -}}
+hostPath:
+ path: {{ .Values.logdir.hostPath }}
+ type: Directory
+{{- else if .Values.logdir.claimName -}}
+persistentVolumeClaim:
+ claimName: {{ .Values.logdir.claimName }}
+{{- else -}}
+emptyDir: {}
+{{- end }}
+{{- end -}}
+
+
+{{/*
+Renders a value that contains template.
+Usage:
+{{ include "cas-server.tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $) }}
+*/}}
+{{- define "cas-server.tplvalues.render" -}}
+ {{- if typeIs "string" .value }}
+ {{- tpl .value .context }}
+ {{- else }}
+ {{- tpl (.value | toYaml) .context }}
+ {{- end }}
+{{- end -}}
diff --git a/helm/cas-server/templates/casconfig-configmap.yaml b/helm/cas-server/templates/casconfig-configmap.yaml
new file mode 100644
index 000000000..3f121681a
--- /dev/null
+++ b/helm/cas-server/templates/casconfig-configmap.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "cas-server.fullname" . }}-casconfig
+ labels: {{- include "cas-server.labels" . | nindent 4 }}
+data:
+ {{- include "cas-server.tplvalues.render" (dict "value" .Values.casServerContainer.casConfig "context" $) | nindent 2 }}
diff --git a/helm/cas-server/templates/ingress.yaml b/helm/cas-server/templates/ingress.yaml
new file mode 100644
index 000000000..b17da9e89
--- /dev/null
+++ b/helm/cas-server/templates/ingress.yaml
@@ -0,0 +1,53 @@
+{{- if .Values.cas.ingress.enabled -}}
+{{- $fullName := include "cas-server.fullname" . -}}
+{{- $svcPort := .Values.cas.service.port -}}
+{{- $kubeVersion := .Capabilities.KubeVersion.Version -}}
+{{- if semverCompare ">=1.19.0" $kubeVersion }}
+apiVersion: networking.k8s.io/v1
+{{- else -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- end }}
+kind: Ingress
+metadata:
+ name: {{ $fullName }}
+ labels:
+ {{- include "cas-server.labels" . | nindent 4 }}
+ {{- with .Values.cas.ingress.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ {{- if .Values.cas.ingress.tls }}
+ tls:
+ {{- range .Values.cas.ingress.tls }}
+ - hosts:
+ {{- range .hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ secretName: {{ .secretName }}
+ {{- end }}
+ {{- end }}
+ rules:
+ {{- range .Values.cas.ingress.hosts }}
+ - host: {{ .host | quote }}
+ http:
+ paths:
+ {{- range .paths }}
+ - path: {{ . }}
+ {{- if semverCompare ">=1.18.0" $kubeVersion }}
+ pathType: Prefix
+ {{- end }}
+ {{- if semverCompare ">=1.19.0" $kubeVersion }}
+ backend:
+ service:
+ name: {{ $fullName }}
+ port:
+ number: {{ $svcPort }}
+ {{- else }}
+ backend:
+ serviceName: {{ $fullName }}
+ servicePort: {{ $svcPort }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
diff --git a/helm/cas-server/templates/role.yaml b/helm/cas-server/templates/role.yaml
new file mode 100644
index 000000000..352265629
--- /dev/null
+++ b/helm/cas-server/templates/role.yaml
@@ -0,0 +1,12 @@
+{{- if .Values.rbac.create -}}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ include "cas-server.fullname" . }}
+ labels:
+ {{- include "cas-server.labels" . | nindent 4 }}
+rules:
+- apiGroups: ["", "extensions", "apps"]
+ resources: ["configmaps", "pods", "services", "endpoints", "secrets"]
+ verbs: ["get", "list", "watch"]
+{{- end -}}
diff --git a/helm/cas-server/templates/rolebinding.yaml b/helm/cas-server/templates/rolebinding.yaml
new file mode 100644
index 000000000..efbd54600
--- /dev/null
+++ b/helm/cas-server/templates/rolebinding.yaml
@@ -0,0 +1,16 @@
+{{- if .Values.rbac.create -}}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ include "cas-server.fullname" . }}
+ labels:
+ {{- include "cas-server.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: {{ include "cas-server.fullname" . }}
+subjects:
+- kind: ServiceAccount
+ name: {{ template "cas-server.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+{{ end }}
diff --git a/helm/cas-server/templates/script-configmap.yaml b/helm/cas-server/templates/script-configmap.yaml
new file mode 100644
index 000000000..0aee4ffae
--- /dev/null
+++ b/helm/cas-server/templates/script-configmap.yaml
@@ -0,0 +1,22 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "cas-server.fullname" . }}-scripts
+ labels: {{- include "cas-server.labels" . | nindent 4 }}
+data:
+ entrypoint.sh: |-
+ #!/bin/sh
+ echo Working Directory: $(pwd)
+ # Set debug options if required
+ JAVA_DEBUG_ARGS=
+ if [ "${JAVA_ENABLE_DEBUG}" == "true" ]; then
+ JAVA_DEBUG_ARGS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=${JAVA_DEBUG_SUSPEND:-n},address=${JAVA_DEBUG_PORT:-5005}"
+ echo "Run the following to forward local port to pod:"
+ echo "kubectl port-forward $HOSTNAME ${JAVA_DEBUG_PORT:-5005}:${JAVA_DEBUG_PORT:-5005}"
+ fi
+ PROFILE_OPT=
+ if [ ! -z $CAS_SPRING_PROFILES ]; then
+ PROFILE_OPT="--spring.profiles.active=$CAS_SPRING_PROFILES"
+ fi
+ echo java -server -noverify $JAVA_DEBUG_ARGS $MAX_HEAP_OPT $NEW_HEAP_OPT $JVM_EXTRA_OPTS -jar $CAS_WAR $PROFILE_OPT $@
+ exec java -server -noverify $JAVA_DEBUG_ARGS $MAX_HEAP_OPT $NEW_HEAP_OPT $JVM_EXTRA_OPTS -jar $CAS_WAR $PROFILE_OPT $@
diff --git a/helm/cas-server/templates/service.yaml b/helm/cas-server/templates/service.yaml
new file mode 100644
index 000000000..a9e9875db
--- /dev/null
+++ b/helm/cas-server/templates/service.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "cas-server.fullname" . }}
+ labels:
+ {{- include "cas-server.labels" . | nindent 4 }}
+spec:
+ type: {{ .Values.cas.service.type }}
+ publishNotReadyAddresses: {{ .Values.cas.service.publishNotReadyAddresses }}
+ ports:
+ - port: {{ .Values.cas.service.port }}
+ targetPort: https
+ protocol: TCP
+ name: https
+ selector:
+ {{- include "cas-server.selectorLabels" . | nindent 4 }}
diff --git a/helm/cas-server/templates/serviceaccount.yaml b/helm/cas-server/templates/serviceaccount.yaml
new file mode 100644
index 000000000..3fef81088
--- /dev/null
+++ b/helm/cas-server/templates/serviceaccount.yaml
@@ -0,0 +1,12 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "cas-server.serviceAccountName" . }}
+ labels:
+ {{- include "cas-server.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+{{- end }}
diff --git a/helm/cas-server/templates/statefulset.yaml b/helm/cas-server/templates/statefulset.yaml
new file mode 100644
index 000000000..595449f60
--- /dev/null
+++ b/helm/cas-server/templates/statefulset.yaml
@@ -0,0 +1,263 @@
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: {{ include "cas-server.fullname" . }}
+ labels: {{- include "cas-server.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ updateStrategy:
+ type: {{ .Values.updateStrategy | quote}}
+ serviceName: {{ include "cas-server.fullname" . }}
+ podManagementPolicy: {{ .Values.podManagementPolicy | quote}}
+ selector:
+ matchLabels:
+ {{- include "cas-server.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ annotations:
+ {{- with .Values.podAnnotations }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{ if .Values.casServerContainer.alwaysRoll }}
+ rollme: {{ randAlphaNum 5 | quote }}
+ {{- else }}
+ rollme: "rolldisabled"
+ {{- end }}
+ labels:
+ {{- include "cas-server.selectorLabels" . | nindent 8 }}
+ spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include "cas-server.serviceAccountName" . }}
+ {{- if .Values.podSecurityContext.enabled }}
+ securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 8 }}
+ {{- end }}
+ volumes:
+ {{- range $.Values.casServerContainer.casConfigMounts }}
+ {{- $configMount := printf "%s-%s" "cas-config" . | replace "." "-" | replace "_" "-" | lower }}
+ - name: {{ $configMount | quote }}
+ configMap:
+ name: {{ include "cas-server.fullname" $ }}-casconfig
+ defaultMode: 0644
+ {{- end }}
+ - name: scripts
+ configMap:
+ name: {{ include "cas-server.fullname" . }}-scripts
+ defaultMode: 0755
+ - name: logdir
+ {{- include "cas-server.logdir" . | nindent 10 }}
+ {{- if .Values.casServerContainer.serverKeystoreExistingSecret }}
+ - name: cas-server-keystore
+ secret:
+ secretName: {{ .Values.casServerContainer.serverKeystoreExistingSecret }}
+ defaultMode: 0444
+ items:
+ - key: {{ .Values.casServerContainer.serverKeystoreSubPath }}
+ path: {{ .Values.casServerContainer.serverKeystoreSubPath }}
+ {{- end }}
+ {{- if .Values.casServerContainer.extraVolumes }}
+ {{- include "cas-server.tplvalues.render" ( dict "value" .Values.casServerContainer.extraVolumes "context" $ ) | nindent 8 }}
+ {{- end }}
+ {{- if or .Values.casServerContainer.initContainers (and .Values.podSecurityContext.enabled .Values.volumePermissions.enabled .Values.persistence.enabled) }}
+ initContainers:
+ {{- if and .Values.podSecurityContext.enabled .Values.volumePermissions.enabled .Values.persistence.enabled }}
+ - name: volume-permissions
+ image: {{ include "cas-server.volumePermissions.image" . }}
+ imagePullPolicy: {{ .Values.volumePermissions.image.pullPolicy | quote }}
+ command:
+ - /bin/sh
+ - -cx
+ - |
+ {{- if .Values.persistence.enabled }}
+ {{- if eq ( toString ( .Values.volumePermissions.securityContext.runAsUser )) "auto" }}
+ chown `id -u`:`id -G | cut -d " " -f2` {{ .Values.persistence.mountPath }}
+ {{- else }}
+ chown {{ .Values.containerSecurityContext.runAsUser }}:{{ .Values.podSecurityContext.fsGroup }} {{ .Values.persistence.mountPath }}
+ {{- end }}
+ mkdir -p {{ .Values.persistence.mountPath }}/data
+ chmod 700 {{ .Values.persistence.mountPath }}/data
+ find {{ .Values.persistence.mountPath }} -mindepth 1 -maxdepth 1 -not -name ".snapshot" -not -name "lost+found" | \
+ {{- if eq ( toString ( .Values.volumePermissions.securityContext.runAsUser )) "auto" }}
+ xargs chown -R `id -u`:`id -G | cut -d " " -f2`
+ {{- else }}
+ xargs chown -R {{ .Values.containerSecurityContext.runAsUser }}:{{ .Values.podSecurityContext.fsGroup }}
+ {{- end }}
+ {{- end }}
+ {{- if eq ( toString ( .Values.volumePermissions.securityContext.runAsUser )) "auto" }}
+ securityContext: {{- omit .Values.volumePermissions.securityContext "runAsUser" | toYaml | nindent 12 }}
+ {{- else }}
+ securityContext: {{- .Values.volumePermissions.securityContext | toYaml | nindent 12 }}
+ {{- end }}
+ {{- if .Values.volumePermissions.resources }}
+ resources: {{- toYaml .Values.volumePermissions.resources | nindent 12 }}
+ {{- end }}
+ volumeMounts:
+ - name: data
+ mountPath: {{ .Values.persistence.mountPath }}
+ {{- end }}
+ {{- if .Values.casServerContainer.initContainers }}
+ {{- include "cas-server.tplvalues.render" (dict "value" .Values.casServerContainer.initContainers "context" $) | nindent 8 }}
+ {{- end }}
+ {{- end }}
+ containers:
+ - name: {{ .Chart.Name }}
+ {{- if .Values.containerSecurityContext.enabled }}
+ securityContext: {{- omit .Values.containerSecurityContext "enabled" | toYaml | nindent 12 }}
+ {{- end }}
+ image: {{ include "cas-server.imageName" . }}
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ env:
+ {{- if .Values.casServerContainer.warPath }}
+ - name: CAS_WAR
+ value: {{ .Values.casServerContainer.warPath | quote }}
+ {{- end }}
+ {{- if .Values.casServerContainer.profiles }}
+ - name: CAS_SPRING_PROFILES
+ value: {{ .Values.casServerContainer.profiles | quote }}
+ {{- end }}
+ {{- if .Values.casServerContainer.jvm.maxHeapOpt }}
+ - name: MAX_HEAP_OPT
+ value: {{ .Values.casServerContainer.jvm.maxHeapOpt | quote }}
+ {{- end }}
+ {{- if .Values.casServerContainer.jvm.minHeapOpt }}
+ - name: MIN_HEAP_OPT
+ value: {{ .Values.casServerContainer.jvm.minHeapOpt | quote }}
+ {{- end }}
+ {{- if .Values.casServerContainer.jvm.extraOpts }}
+ - name: JVM_EXTRA_OPTS
+ value: {{ .Values.casServerContainer.jvm.extraOpts | quote }}
+ {{- end }}
+ - name: JAVA_ENABLE_DEBUG
+ value: {{ .Values.casServerContainer.jvm.debugEnabled | quote }}
+ - name: JAVA_DEBUG_SUSPEND
+ value: {{ .Values.casServerContainer.jvm.debugSuspend | quote }}
+ - name: 'KUBERNETES_NAMESPACE' # used by org.apache.catalina.tribes.membership.cloud.CloudMembershipProvider
+ value: {{ .Release.Namespace }}
+ - name: 'POD_IP'
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ {{- if .Values.casServerContainer.extraEnvVars }}
+ {{- include "cas-server.tplvalues.render" (dict "value" .Values.casServerContainer.extraEnvVars "context" $) | nindent 12 }}
+ {{- end }}
+ envFrom:
+ {{- if .Values.casServerContainer.extraEnvVarsConfigMap }}
+ - configMapRef:
+ name: {{ .Values.casServerContainer.extraEnvVarsConfigMap }}
+ {{- end }}
+ {{- if .Values.casServerContainer.extraEnvVarsSecret }}
+ - secretRef:
+ name: {{ .Values.casServerContainer.extraEnvVarsSecret }}
+ {{- end }}
+ {{- if .Values.casServerContainer.command }}
+ command: {{- include "cas-server.tplvalues.render" (dict "value" .Values.casServerContainer.command "context" $) | nindent 12 }}
+ {{- else }}
+ command:
+ - '/entrypoint.sh'
+ {{- end }}
+ {{- if .Values.casServerContainer.args }}
+ args: {{- include "cas-server.tplvalues.render" (dict "value" .Values.casServerContainer.args "context" $) | nindent 12 }}
+ {{- end }}
+ ports:
+ - name: https
+ containerPort: {{ .Values.cas.listenPortHttps }}
+ protocol: TCP
+ - name: jvm-debug
+ containerPort: {{ .Values.cas.listenPortJvmDebug }}
+ protocol: TCP
+ volumeMounts:
+ {{- if .Values.persistence.enabled }}
+ - name: data
+ mountPath: {{ .Values.persistence.mountPath }}
+ {{- end }}
+ {{- range $.Values.casServerContainer.casConfigMounts }}
+ {{- $configMount := printf "%s-%s" "cas-config" . | replace "." "-" | replace "_" "-" | lower }}
+ {{- $configMountPath := printf "%s/%s" "/etc/cas/config" . }}
+ - name: {{ $configMount | quote }}
+ mountPath: {{ $configMountPath }}
+ subPath: {{ . | quote }}
+ {{- end }}
+ - name: scripts
+ mountPath: /entrypoint.sh
+ subPath: entrypoint.sh
+ - name: logdir
+ mountPath: {{ .Values.logdir.mountPath }}
+ {{- if .Values.casServerContainer.serverKeystoreExistingSecret }}
+ - name: cas-server-keystore
+ mountPath: {{ .Values.casServerContainer.serverKeystoreMountPath }}
+ subPath: {{ .Values.casServerContainer.serverKeystoreSubPath }}
+ {{- end }}
+ {{- if .Values.casServerContainer.extraVolumeMounts }}
+ {{- include "cas-server.tplvalues.render" ( dict "value" .Values.casServerContainer.extraVolumeMounts "context" $ ) | nindent 12 }}
+ {{- end }}
+ startupProbe:
+ httpGet:
+ path: {{ .Values.casServerContainer.defaultStatusUrl }}
+ port: https
+ scheme: HTTPS
+ {{- if .Values.casServerContainer.defaultStatusHeaders }}
+ {{- include "cas-server.tplvalues.render" ( dict "value" .Values.casServerContainer.defaultStatusHeaders "context" $ ) | nindent 14 }}
+ {{- end }}
+ failureThreshold: {{ .Values.casServerContainer.startupFailureThreshold }}
+ periodSeconds: 20
+ readinessProbe:
+ httpGet:
+ path: {{ .Values.casServerContainer.defaultStatusUrl }}
+ port: https
+ scheme: HTTPS
+ {{- if .Values.casServerContainer.defaultStatusHeaders }}
+ {{- include "cas-server.tplvalues.render" ( dict "value" .Values.casServerContainer.defaultStatusHeaders "context" $ ) | nindent 14 }}
+ {{- end }}
+ initialDelaySeconds: {{ .Values.casServerContainer.readinessInitialDelaySeconds }}
+ periodSeconds: 5
+ failureThreshold: {{ .Values.casServerContainer.readinessFailureThreshold }}
+ livenessProbe:
+ httpGet:
+ path: {{ .Values.casServerContainer.defaultStatusUrl }}
+ port: https
+ scheme: HTTPS
+ {{- if .Values.casServerContainer.defaultStatusHeaders }}
+ {{- include "cas-server.tplvalues.render" ( dict "value" .Values.casServerContainer.defaultStatusHeaders "context" $ ) | nindent 14 }}
+ {{- end }}
+ initialDelaySeconds: {{ .Values.casServerContainer.livenessInitialDelaySeconds }}
+ periodSeconds: 15
+ failureThreshold: {{ .Values.casServerContainer.livenessFailureThreshold }}
+ resources:
+ {{- toYaml .Values.resources | nindent 12 }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+{{- if .Values.persistence.enabled }}
+ volumeClaimTemplates:
+ - metadata:
+ name: data
+ {{- with .Values.persistence.annotations }}
+ annotations:
+ {{- range $key, $value := . }}
+ {{ $key }}: {{ $value }}
+ {{- end }}
+ {{- end }}
+ spec:
+ accessModes:
+ {{- range .Values.persistence.accessModes }}
+ - {{ . | quote }}
+ {{- end }}
+ resources:
+ requests:
+ storage: {{ .Values.persistence.size | quote }}
+ storageClassName: {{ .Values.persistence.storageClassName | quote }}
+ {{- if .Values.persistence.selector }}
+ selector: {{- include "cas-server.tplvalues.render" (dict "value" .Values.persistence.selector "context" $) | nindent 10 }}
+ {{- end -}}
+{{- end }}
\ No newline at end of file
diff --git a/helm/cas-server/templates/tests/test-cas-server.yaml b/helm/cas-server/templates/tests/test-cas-server.yaml
new file mode 100644
index 000000000..2ca0f1403
--- /dev/null
+++ b/helm/cas-server/templates/tests/test-cas-server.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: "{{ include "cas-server.fullname" . }}-test"
+ labels:
+ {{- include "cas-server.labels" . | nindent 4 }}
+ annotations:
+ "helm.sh/hook": test-success
+spec:
+ containers:
+ - name: wget
+ image: alpine
+ command: ['wget']
+ args: [ '--no-check-certificate', 'https://{{ include "cas-server.fullname" . }}:{{ .Values.cas.service.port }}{{ .Values.casServerContainer.defaultStatusUrl }}' ]
+ restartPolicy: Never
+
diff --git a/helm/cas-server/values.yaml b/helm/cas-server/values.yaml
new file mode 100644
index 000000000..b9bced1f9
--- /dev/null
+++ b/helm/cas-server/values.yaml
@@ -0,0 +1,371 @@
+# Default values for cas-server.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+casServerName: cas.example.org
+
+replicaCount: 1
+
+image:
+ registry: ""
+ repository: "apereo/cas"
+ pullPolicy: IfNotPresent
+ # Overrides the image tag whose default is the chart appVersion.
+ tag: "latest"
+
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+# There are two valid stateful set update strategies, RollingUpdate and the (legacy) OnDelete
+updateStrategy: RollingUpdate
+
+# OrderedReady: Pods are created in increasing order (pod-0, then pod-1, etc) and the controller will wait until each pod is ready before continuing.
+# When scaling down, the pods are removed in the opposite order.
+# Parallel: Creates pods in parallel to match the desired scale without waiting, and on scale down will delete all pods at once.
+podManagementPolicy: OrderedReady
+
+# Map folder for logs directory from host or pvc, or leave both blank to use emptyDir volume
+# In docker for windows hostPath could be '/host_mnt/c/opt/cas/logs'
+# Windows: Give full access local Users group to the to ~/.docker folder if getting permission denied)
+logdir:
+# hostPath: '/host_mnt/c/opt/cas/logs'
+ hostPath: ''
+ claimName: ''
+ mountPath: '/var/log'
+
+# CAS Server container properties
+casServerContainer:
+ ## Roll on upgrade changes deployment when helm upgrade runs, forcing pod to restart
+ alwaysRoll: false
+ ## JVM Settings
+ ## JVM settings only used if command not set, use args to set app arguments
+ jvm:
+ ## Extra JVM options
+ ##
+ extraOpts: '-Djavax.net.ssl.trustStore=/etc/cas/truststore -Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStorePassword=changeit'
+
+ ## Memory settings: If these aren't defined, java will calc values automatically, but requires setting limits on pod
+ ## so it doesn't base heap size on host memory
+ maxHeapOpt: '-Xmx2G'
+ newHeapOpt: '-Xms600M'
+ debugEnabled: true
+ debugSuspend: "n" # could be n or y, must quote or yaml changes to boolean
+ warPath: 'cas.war'
+ ## Override cmd
+ ##
+ command:
+ ## Override args
+ ##
+ args:
+ ## extraVolumes and extraVolumeMounts allows you to mount other volumes
+ ## Examples:
+ ## extraVolumeMounts:
+ ## - name: extras
+ ## mountPath: /usr/share/extras
+ ## readOnly: true
+ ## extraVolumes:
+ ## - name: extras
+ ## emptyDir: {}
+ ##
+ profiles: 'standalone'
+
+ extraVolumeMounts:
+ - name: truststore
+ mountPath: /etc/cas/truststore
+ subPath: truststore
+
+ extraVolumes:
+ - name: truststore
+ configMap:
+ name: cas-truststore
+ defaultMode: 0444
+
+ ## Url to use for readiness, startupprobe, and liveliness check, change to health actuator if the module is available
+ ## Naming it "default" in case in future template supports individual urls for the different checks, with this as default if they aren't specified
+ defaultStatusUrl: '/cas/actuator/health'
+
+ # number of startup probe failures before it will be killed, set high if trying to debug startup issues
+ # liveness and readiness failure threshold might be 1 but startup failure threshold accounts for
+ # failures while server is starting up
+ startupFailureThreshold: 30
+ livenessFailureThreshold: 1
+ readinessFailureThreshold: 1
+ readinessInitialDelaySeconds: 45
+ livenessInitialDelaySeconds: 120
+
+ ## Extra init containers to add to the statefulset
+ ##
+ initContainers: []
+
+ ## An array to add extra env vars
+ ## For example:
+ ## extraEnvVars:
+ ## - name: MY_ENV_VAR
+ ## value: env_var_value
+ ##
+ extraEnvVars: []
+
+ ## Name of a ConfigMap containing extra env vars
+ ##
+ extraEnvVarsConfigMap: ''
+
+ # name of secret containing server keystore
+ serverKeystoreExistingSecret: cas-server-keystore
+ # folder that should container the keystore
+ serverKeystoreMountPath: '/etc/cas/thekeystore'
+ # name of keystore file in container and in secret
+ serverKeystoreSubPath: 'thekeystore'
+
+ ## Name of a Secret containing extra env vars
+ ##
+ extraEnvVarsSecret: ''
+ ## Choose which config files from casConfig to mount
+ casConfigMounts:
+ - 'cas.properties'
+ - 'cas.yaml'
+ ## Create various config files from casConfig that may or may not be mounted
+ casConfig:
+ # issue with line breaks? means can't use {{}} variables after first line
+ # workaround is to use {{}} variables in yaml version of properties file
+ cas.properties: |-
+ cas.server.name=https://{{ .Values.casServerName }}
+ context.path=/cas
+ cas.server.prefix=${cas.server.name}${context.path}
+
+ cas.http-client.truststore.psw=changeit
+ cas.http-client.truststore.file=/etc/cas/truststore
+
+ # put web access logs in same directory as cas logs
+ cas.server.tomcat.ext-access-log.directory=/var/log
+ cas.server.tomcat.ext-access-log.enabled=true
+
+ # uncomment the folowing to not allow login of built-in users
+ # cas.authn.accept.users=
+
+ # since we are behind ingress controller, need to use x-forwarded-for to get client ip
+ # if nginx ingress controller is behind another proxy, it needs to be configured globally with the following settings in the ingress controller configmap
+ # use-forwarded-headers: "true" # very important for CAS or any app that compares IP being used against IP that initiated sessions (session fixation)
+ # enable-underscores-in-headers: "true" # while you are at it, allow underscores in headers, can't recall if important for cas but no need to have nginx dropping your headers with underscores
+ cas.audit.engine.alternate-client-addr-header-name=X-Forwarded-For
+ server.tomcat.remoteip.remote-ip-header=X-FORWARDED-FOR
+
+ server.ssl.key-store=file:/etc/cas/thekeystore
+ server.ssl.key-store-type=PKCS12
+ server.ssl.key-store-password=changeit
+ server.ssl.trust-store=file:/etc/cas/truststore
+ server.ssl.trust-store-type=PKCS12
+ server.ssl.trust-store-password=changeit
+
+ # expose endpoints via http
+ management.endpoints.web.exposure.include=health,info,prometheus,metrics,env,loggers,statistics,status,loggingConfig,events,configurationMetadata,caches
+ management.endpoints.web.base-path=/actuator
+ management.endpoints.web.cors.allowed-origins=https://${cas-host}
+ management.endpoints.web.cors.allowed-methods=GET,POST
+
+ # enable endpoints
+ management.endpoint.metrics.enabled=true
+ management.endpoint.health.enabled=true
+ management.endpoint.info.enabled=true
+ management.endpoint.env.enabled=true
+ management.endpoint.loggers.enabled=true
+ management.endpoint.status.enabled=true
+ management.endpoint.statistics.enabled=true
+ management.endpoint.prometheus.enabled=true
+ management.endpoint.events.enabled=true
+ management.endpoint.loggingConfig.enabled=true
+ management.endpoint.configurationMetadata.enabled=true
+ # configure health endpoint
+ management.health.defaults.enabled=false
+ management.health.ping.enabled=true
+ management.health.caches.enabled=true
+
+ # secure endpoints to localhost
+
+ cas.monitor.endpoints.endpoint.defaults.access[0]=AUTHENTICATED
+ cas.monitor.endpoints.endpoint.health.access[0]=IP_ADDRESS
+ cas.monitor.endpoints.endpoint.health.requiredIpAddresses[0]=127.0.0.1
+ cas.monitor.endpoints.endpoint.health.requiredIpAddresses[1]=0:0:0:0:0:0:0:1
+ cas.monitor.endpoints.endpoint.health.requiredIpAddresses[2]=10\\..*
+ cas.monitor.endpoints.endpoint.health.requiredIpAddresses[3]=172\\.16\\..*
+ cas.monitor.endpoints.endpoint.health.requiredIpAddresses[4]=192\\.168\\..*
+ #eof
+
+ cas.yaml: |-
+ ---
+ logging:
+ config: 'file:/etc/cas/config/log4j2.xml'
+ cas:
+ server:
+ tomcat:
+ clustering:
+ enabled: true
+ clustering-type: 'CLOUD'
+ cloud-membership-provider: 'kubernetes'
+ spring:
+ security:
+ user:
+ name: "{{ .Values.casAdminUser }}"
+ password: "{{ .Values.casAdminPassword }}"
+ #eof
+
+podAnnotations: {}
+
+## Pod security context
+## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
+##
+podSecurityContext:
+ enabled: true
+ fsGroup: 1000
+
+containerSecurityContext:
+ enabled: false
+ # capabilities:
+ # drop:
+ # - ALL
+ # readOnlyRootFilesystem: true
+ # runAsNonRoot: true
+ runAsUser: 1000
+
+## Override parts of this ingress in your own values file with appropriate host names
+## This currently is only set up to work with Nginx Ingress Controller from Kubernetes project
+cas:
+ service:
+ type: ClusterIP
+ publishNotReadyAddresses: true
+ port: 8443
+ listenPortHttps: 8443
+ listenPortJvmDebug: 5005
+ ingress:
+ enabled: true
+ annotations:
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/session-cookie-samesite: "None"
+ nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none: "true"
+ nginx.ingress.kubernetes.io/affinity: "cookie"
+ nginx.ingress.kubernetes.io/session-cookie-name: "sticky-session-route"
+ nginx.ingress.kubernetes.io/session-cookie-hash: "sha1"
+ nginx.ingress.kubernetes.io/secure-backends: "true"
+ nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
+ hosts:
+ - host: cas.example.org
+ paths:
+ - "/cas"
+ - host: kubernetes.docker.internal
+ paths:
+ - "/cas"
+ tls:
+ - secretName: cas-server-ingress-tls
+ hosts:
+ - cas.example.org
+ - kubernetes.docker.internal
+
+# Request some resources for main cas server so kubernetes will schedule somewhere with enough resources
+# Limits can also be set if desired
+resources:
+ requests:
+ cpu: 100m
+ memory: 512Mi
+# limits:
+# cpu: 100m
+# memory: 128Mi
+
+# node selector for CAS server
+nodeSelector: {}
+# tolerations for CAS server (i.e taints on nodes that it can tolerate)
+tolerations: []
+# affinity config for CAS server
+affinity: {}
+
+casAdminUser: 'casuser'
+casAdminPassword: 'Mellon'
+
+# rbac may or may not be necessary, but it can allow for certain types of discovery (e.g. tomcat cloud session replication)
+rbac:
+ # specified whether RBAC resources should be created
+ create: true
+
+serviceAccount:
+ # Specifies whether a service account should be created
+ create: true
+ # Annotations to add to the service account
+ annotations: {}
+ # The name of the service account to use.
+ # If not set and create is true, a name is generated using the fullname template
+ name: ""
+
+
+## CAS can use a persistent volume to store config such as services and saml IDP/SP metadata that it pulls from git
+## Enable persistence using Persistent Volume Claims
+## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
+##
+persistence:
+ ## If true, use a Persistent Volume Claim for data folder mounted where you specify using mountPath
+ ##
+ enabled: true
+ ## Persistent Volume Storage Class
+ ## If defined, storageClassName:
+ ## If set to "-", storageClassName: "", which disables dynamic provisioning
+ ## If undefined (the default) or set to null, no storageClassName spec is
+ ## set, choosing the default provisioner. (gp2 on AWS, standard on
+ ## GKE, AWS & OpenStack)
+ ##
+ # storageClass: "-"
+ ## Persistent Volume Claim annotations
+ ##
+ annotations:
+ ## Persistent Volume Access Mode
+ ##
+ accessModes:
+ - ReadWriteOnce
+ ## Persistent Volume size
+ ##
+ size: 2Gi
+ ## The path the volume will be mounted at, will contain writable folder called "data" under mountPath,
+ ## if volumePermissions init container creates it
+ ##
+ mountPath: /var/cas
+
+## Init containers parameters:
+## volumePermissions: Change the owner and group of the persistent volume mountpoint to runAsUser:fsGroup values from
+## the securityContext section.
+##
+volumePermissions:
+ enabled: false
+ image:
+ registry: docker.io
+ repository: alpine
+ tag: latest
+ pullPolicy: Always
+ ## Optionally specify an array of imagePullSecrets.
+ ## Secrets must be manually created in the namespace.
+ ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+ ##
+ # pullSecrets:
+ # - myRegistryKeySecretName
+ ## Init container' resource requests and limits
+ ## ref: http://kubernetes.io/docs/user-guide/compute-resources/
+ ##
+ resources:
+ # We usually recommend not to specify default resources and to leave this as a conscious
+ # choice for the user. This also increases chances charts run on environments with little
+ # resources, such as Minikube. If you do want to specify resources, uncomment the following
+ # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+ limits: {}
+ # cpu: 100m
+ # memory: 128Mi
+ requests: {}
+ # cpu: 100m
+ # memory: 128Mi
+ ## Init container Security Context
+ ## Note: the chown of the data folder is done to securityContext.runAsUser
+ ## and not the below volumePermissions.securityContext.runAsUser
+ ## When runAsUser is set to special value "auto", init container will try to chown the
+ ## data folder to autodetermined user&group, using commands: `id -u`:`id -G | cut -d" " -f2`
+ ## "auto" is especially useful for OpenShift which has scc with dynamic userids (and 0 is not allowed).
+ ## You may want to use this volumePermissions.securityContext.runAsUser="auto" in combination with
+ ## pod securityContext.enabled=false and shmVolume.chmod.enabled=false
+ ##
+ securityContext:
+ runAsUser: 0
diff --git a/helm/create-cas-server-keystore-secret.sh b/helm/create-cas-server-keystore-secret.sh
new file mode 100755
index 000000000..68dd3d4a5
--- /dev/null
+++ b/helm/create-cas-server-keystore-secret.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# This script needs bash for pushd/popd
+set -e
+NAMESPACE=${1:-default}
+KEYSTORE=../etc/cas/thekeystore
+
+# it's important that the service names are supported in the cert used for tomcat in cas-server
+# keytool doesn't support wildcards which we really need to use here, e.g. *.cas-server.${NAMESPACE}.svc
+# java wasn't resolving using all available dns suffixes so had to use [namespace].svc
+SUBJECT=CN=cas.example.org,OU=Example,OU=Org,C=US
+SAN=dns:cas.example.org,dns:casadmin.example.org,dns:cas-server-0.cas-server.${NAMESPACE}.svc,dns:cas-server-1.cas-server.${NAMESPACE}.svc
+
+if [ ! -f "$KEYSTORE" ] ; then
+ pushd ..
+ ./gradlew --no-configuration-cache createKeyStore -PcertDir=./etc/cas -PcertificateDn="${SUBJECT}" -PcertificateSubAltName="${SAN}"
+ popd
+fi
+
+kubectl delete secret cas-server-keystore --namespace "${NAMESPACE}" || true
+kubectl create secret generic cas-server-keystore --namespace "${NAMESPACE}" --from-file=thekeystore=$KEYSTORE
diff --git a/helm/create-ingress-tls.sh b/helm/create-ingress-tls.sh
new file mode 100755
index 000000000..f2c5c7fad
--- /dev/null
+++ b/helm/create-ingress-tls.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+NAMESPACE=${1:-default}
+SUBJECT=/CN=cas.example.org/OU=Auth/O=example
+SAN=DNS:casadmin.example.org,DNS:cas.example.org
+SECRET_NAME=cas-server-ingress-tls
+KEY_FILE=cas-ingress.key
+CERT_FILE=cas-ingress.crt
+
+set -e
+
+# create certificate for external ingress
+openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+ -keyout "${KEY_FILE}" -out ${CERT_FILE} -subj "${SUBJECT}" \
+ -addext "subjectAltName = $SAN"
+
+kubectl delete secret "${SECRET_NAME}" --namespace "${NAMESPACE}" || true
+# create tls secret with key and cert
+kubectl create secret tls "${SECRET_NAME}" --namespace "${NAMESPACE}" --key "${KEY_FILE}" --cert "${CERT_FILE}"
+
diff --git a/helm/create-truststore.sh b/helm/create-truststore.sh
new file mode 100755
index 000000000..ebd01cf55
--- /dev/null
+++ b/helm/create-truststore.sh
@@ -0,0 +1,42 @@
+#!/bin/sh
+NAMESPACE=${1:-default}
+INGRESS_CERT_FILE=cas-ingress.crt
+CAS_CERT_FILE=cas.crt
+CAS_KEYSTORE=../etc/cas/thekeystore
+TRUST_STORE=../etc/cas/truststore
+JAVA_CACERTS=${2:-/etc/ssl/certs/java/cacerts}
+
+STORE_PASS=changeit
+
+set -e
+
+if [ -f ${TRUST_STORE} ]; then
+ rm ${TRUST_STORE}
+fi
+
+if [ -f "${JAVA_CACERTS}" ]; then
+ keytool -importkeystore -noprompt -srckeystore "${JAVA_CACERTS}" -srcstorepass "${STORE_PASS}" -destkeystore "${TRUST_STORE}" -deststoretype PKCS12 -deststorepass "${STORE_PASS}"
+else
+ echo "Missing ${JAVA_CACERTS} JAVA_HOME is ${JAVA_HOME}"
+ if [ -d "${JAVA_HOME}" ]; then
+ find ${JAVA_HOME} -name cacerts -print
+ find ${JAVA_HOME} -name cacerts -exec keytool -importkeystore -noprompt -srckeystore {} -srcstorepass "${STORE_PASS}" -destkeystore "${TRUST_STORE}" -deststoretype PKCS12 -deststorepass "${STORE_PASS}" \;
+ fi
+fi
+
+# create truststore that trusts ingress cert
+if [ -f "${INGRESS_CERT_FILE}" ] ; then
+ keytool -importcert -noprompt -keystore "${TRUST_STORE}" -storepass "${STORE_PASS}" -alias cas-ingress -file "${INGRESS_CERT_FILE}" -storetype PKCS12
+else
+ echo "Missing ingress cert file to put in trust bundle: ${INGRESS_CERT_FILE}"
+fi
+
+# add cas server cert to trust store
+if [ -f "${CAS_KEYSTORE}" ] ; then
+ keytool -exportcert -keystore "${CAS_KEYSTORE}" -storepass "${STORE_PASS}" -alias cas -file "${CAS_CERT_FILE}" -rfc
+ keytool -importcert -noprompt -storepass "${STORE_PASS}" -keystore "${TRUST_STORE}" -alias cas -file "${CAS_CERT_FILE}" -storetype PKCS12
+else
+ echo "Missing keystore ${CAS_KEYSTORE} to put cas cert in trust bundle"
+fi
+kubectl delete configmap cas-truststore --namespace "${NAMESPACE}" || true
+kubectl create configmap cas-truststore --namespace "${NAMESPACE}" --from-file=truststore=${TRUST_STORE}
\ No newline at end of file
diff --git a/helm/delete-cas-server.sh b/helm/delete-cas-server.sh
new file mode 100755
index 000000000..3a8269d0a
--- /dev/null
+++ b/helm/delete-cas-server.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+NAMESPACE=${1:-default}
+helm delete --namespace "${NAMESPACE}" cas-server
\ No newline at end of file
diff --git a/helm/install-cas-server-example.sh b/helm/install-cas-server-example.sh
new file mode 100755
index 000000000..65ecde059
--- /dev/null
+++ b/helm/install-cas-server-example.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+NAMESPACE=${1:-default}
+EXAMPLE=${2:-example1}
+
+helm upgrade --install cas-server --values values-${EXAMPLE}.yaml --namespace ${NAMESPACE} ./cas-server
diff --git a/helm/install-cas-server.sh b/helm/install-cas-server.sh
new file mode 100755
index 000000000..5e94d3a12
--- /dev/null
+++ b/helm/install-cas-server.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+NAMESPACE=${1:-default}
+
+helm upgrade --install cas-server --namespace $NAMESPACE ./cas-server
\ No newline at end of file
diff --git a/helm/values-example1.yaml b/helm/values-example1.yaml
new file mode 100644
index 000000000..53e2f73a5
--- /dev/null
+++ b/helm/values-example1.yaml
@@ -0,0 +1,63 @@
+---
+
+# This is example of a values file that can override and add to the default values.yaml
+# Deployers might have one or more values files of their own per deployment environment.
+
+# CAS Server container properties
+casServerContainer:
+
+ # override profiles to include gitsvc
+ profiles: 'standalone,gitsvc'
+
+ ## Override list of config files from casConfig to mount, include some from default values file
+ casConfigMounts:
+ - 'cas.properties'
+ - 'cas.yaml'
+ - 'application-gitsvc.yaml'
+ casConfig:
+ application-gitsvc.yaml: |-
+ ---
+ cas:
+ service-registry:
+ git:
+ repository-url: "{{- .Values.gitsvcRepoUrl -}}"
+ branches-to-clone: "{{- .Values.gitsvcBranchesToClone -}}"
+ active-branch: "{{- .Values.gitsvcActiveBranch -}}"
+ clone-directory: "{{- .Values.gitsvcCloneDirectory -}}"
+ root-directory: "{{- .Values.gitsvcRootDirectory -}}"
+ #eof
+ application-redis.yaml: |-
+ ---
+ #helm repo add bitnami https://charts.bitnami.com/bitnami
+ #helm install cas-server-redis bitnami/redis --set usePassword=false --set sentinel.enabled=true --set sentinel.usePassword=false
+ cas:
+ ticket:
+ registry:
+ redis:
+ enabled: true
+ database: 0
+ host: 'cas-server-redis'
+ pool:
+ test-on-borrow: true
+ read-from: 'UPSTREAMPREFERRED'
+ crypto:
+ enabled: false
+ timeout: 5000
+ port: 6379
+ password: ' '
+ cluster:
+ nodes:
+ - host: 'cas-server-redis-headless'
+ port: 6379
+ password: ' '
+ sentinel:
+ master: 'mymaster'
+ node: 'cas-server-redis-headless:26379'
+ # eof
+
+
+gitsvcRepoUrl: 'https://github.com/apereo/cas.git' # need smaller repo with services
+gitsvcBranchesToClone: 'master'
+gitsvcActiveBranch: 'master'
+gitsvcCloneDirectory: '/tmp/cas/services'
+gitsvcRootDirectory: 'etc' # only supports one level
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 000000000..f562841cc
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1,9 @@
+lombok.log.fieldName = LOGGER
+lombok.log.fieldIsStatic=true
+
+lombok.toString.doNotUseGetters=true
+lombok.equalsAndHashCode.doNotUseGetters=true
+
+lombok.addLombokGeneratedAnnotation = true
+
+config.stopBubbling=true
diff --git a/openrewrite.gradle b/openrewrite.gradle
new file mode 100644
index 000000000..48c1284f3
--- /dev/null
+++ b/openrewrite.gradle
@@ -0,0 +1,25 @@
+initscript {
+ repositories {
+ gradlePluginPortal()
+ }
+ dependencies {
+ classpath "org.openrewrite:plugin:6.28.1"
+ }
+}
+
+rootProject {
+ plugins.apply(org.openrewrite.gradle.RewritePlugin)
+ dependencies {
+ rewrite("org.apereo.cas:cas-server-support-openrewrite:${project.targetVersion}") {
+ transitive = false
+ }
+ }
+ afterEvaluate {
+ if (repositories.isEmpty()) {
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ }
+ }
+ }
+}
diff --git a/puppeteer/package.json b/puppeteer/package.json
new file mode 100644
index 000000000..ab27112ed
--- /dev/null
+++ b/puppeteer/package.json
@@ -0,0 +1,7 @@
+{
+ "dependencies": {
+ "pino-pretty": "13.0.0",
+ "pino": "9.5.0",
+ "puppeteer": "23.10.4"
+ }
+}
diff --git a/puppeteer/run.sh b/puppeteer/run.sh
new file mode 100644
index 000000000..d5a7632db
--- /dev/null
+++ b/puppeteer/run.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+CAS_ARGS="${CAS_ARGS:-}"
+
+RED="\e[31m"
+GREEN="\e[32m"
+YELLOW="\e[33m"
+ENDCOLOR="\e[0m"
+
+function printgreen() {
+ printf "${GREEN}$1${ENDCOLOR}\n"
+}
+function printyellow() {
+ printf "${YELLOW}$1${ENDCOLOR}\n"
+}
+function printred() {
+ printf "${RED}$1${ENDCOLOR}\n"
+}
+
+casWebApplicationFile="${PWD}/build/libs/cas.war"
+if [[ ! -f "$casWebApplicationFile" ]]; then
+ echo "Building CAS"
+ ./gradlew clean build -x test -x javadoc --no-configuration-cache --offline
+ if [ $? -ne 0 ]; then
+ printred "Failed to build CAS"
+ exit 1
+ fi
+fi
+
+if [[ ! -d "${PWD}/puppeteer/node_modules/puppeteer" ]]; then
+ echo "Installing Puppeteer"
+ (cd "${PWD}/puppeteer" && npm install puppeteer)
+else
+ echo "Using existing Puppeteer modules..."
+fi
+
+echo -n "NPM version: " && npm --version
+echo -n "Node version: " && node --version
+
+echo "Launching CAS at $casWebApplicationFile with options $CAS_ARGS"
+java -jar "$casWebApplicationFile" $CAS_ARGS &
+pid=$!
+echo "Waiting for CAS under process id ${pid}"
+sleep 45
+casLogin="${PUPPETEER_CAS_HOST:-https://localhost:8443}/cas/login"
+echo "Checking CAS status at ${casLogin}"
+curl -k -L --output /dev/null --silent --fail "$casLogin"
+if [[ $? -ne 0 ]]; then
+ printred "Unable to launch CAS instance under process id ${pid}."
+ printred "Killing process id $pid and exiting"
+ kill -9 "$pid"
+ exit 1
+fi
+
+export NODE_TLS_REJECT_UNAUTHORIZED=0
+echo "Executing puppeteer scenarios..."
+for scenario in "${PWD}"/puppeteer/scenarios/*; do
+ scenarioName=$(basename "$scenario")
+ echo "=========================="
+ echo "- Scenario $scenarioName "
+ echo -e "==========================\n"
+ node "$scenario"
+ rc=$?
+ echo -e "\n"
+ if [[ $rc -ne 0 ]]; then
+ printred "🔥 Scenario $scenarioName FAILED"
+ else
+ printgreen "✅ Scenario $scenarioName PASSED"
+ fi
+ echo -e "\n"
+ sleep 1
+done;
+
+kill -9 "$pid"
+exit 0
diff --git a/puppeteer/scenarios/basic.js b/puppeteer/scenarios/basic.js
new file mode 100644
index 000000000..10b39594d
--- /dev/null
+++ b/puppeteer/scenarios/basic.js
@@ -0,0 +1,51 @@
+const puppeteer = require('puppeteer');
+const assert = require("assert");
+const pino = require('pino');
+const logger = pino({
+ level: "info",
+ transport: {
+ target: 'pino-pretty'
+ }
+});
+
+(async () => {
+ const browser = await puppeteer.launch({
+ headless: (process.env.CI === "true" || process.env.HEADLESS === "true") ? "new" : false,
+ ignoreHTTPSErrors: true,
+ devtools: false,
+ defaultViewport: null,
+ slowMo: 5,
+ args: ['--start-maximized', "--window-size=1920,1080"]
+ });
+
+ try {
+ const page = await browser.newPage();
+
+ const casHost = process.env.PUPPETEER_CAS_HOST || "https://localhost:8443";
+ await page.goto(`${casHost}/cas/login`);
+
+ await page.waitForSelector("#username", {visible: true});
+ await page.$eval("#username", el => el.value = '');
+ await page.type("#username", "casuser");
+
+ await page.waitForSelector("#password", {visible: true});
+ await page.$eval("#password", el => el.value = '');
+ await page.type("#password", "Mellon");
+
+ await page.keyboard.press('Enter');
+ await page.waitForNavigation();
+
+ const cookies = (await page.cookies()).filter(c => {
+ logger.debug(`Checking cookie ${c.name}:${c.value}`);
+ return c.name === "TGC";
+ });
+ assert(cookies.length !== 0);
+ logger.info(`Cookie:\n${JSON.stringify(cookies, undefined, 2)}`);
+ await process.exit(0)
+ } catch (e) {
+ logger.error(e);
+ await process.exit(1)
+ } finally {
+ await browser.close();
+ }
+})();
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..c15bea350
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,4 @@
+plugins {
+ id "org.gradle.toolchains.foojay-resolver-convention" version "${gradleFoojayPluginVersion}"
+}
+rootProject.name = 'cas'
diff --git a/src/main/java/org/apereo/cas/config/CasOverlayOverrideConfiguration.java b/src/main/java/org/apereo/cas/config/CasOverlayOverrideConfiguration.java
new file mode 100644
index 000000000..eb7ec10bd
--- /dev/null
+++ b/src/main/java/org/apereo/cas/config/CasOverlayOverrideConfiguration.java
@@ -0,0 +1,23 @@
+package org.apereo.cas.config;
+
+//import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+
+//import org.apereo.cas.configuration.CasConfigurationProperties;
+
+@AutoConfiguration
+//@EnableConfigurationProperties(CasConfigurationProperties.class)
+public class CasOverlayOverrideConfiguration {
+
+ /*
+ @Bean
+ public MyCustomBean myCustomBean() {
+ ...
+ }
+ */
+}
diff --git a/src/main/jib/docker/entrypoint.sh b/src/main/jib/docker/entrypoint.sh
new file mode 100755
index 000000000..2747c3fa3
--- /dev/null
+++ b/src/main/jib/docker/entrypoint.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+ENTRYPOINT_DEBUG=${ENTRYPOINT_DEBUG:-false}
+JVM_DEBUG=${JVM_DEBUG:-false}
+JVM_DEBUG_PORT=${JVM_DEBUG_PORT:-5000}
+JVM_DEBUG_SUSPEND=${JVM_DEBUG_SUSPEND:-n}
+JVM_MEM_OPTS=${JVM_MEM_OPTS:--Xms512m -Xmx4096M}
+JVM_EXTRA_OPTS=${JVM_EXTRA_OPTS:--server -noverify -XX:+TieredCompilation -XX:TieredStopAtLevel=1}
+
+if [ $JVM_DEBUG = "true" ]; then
+ JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} -Xdebug -Xrunjdwp:transport=dt_socket,address=*:${JVM_DEBUG_PORT},server=y,suspend=${JVM_DEBUG_SUSPEND}"
+fi
+
+if [ $ENTRYPOINT_DEBUG = "true" ]; then
+ JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} -Ddebug=true"
+
+ echo "\nChecking java..."
+ java -version
+
+ if [ -d /etc/cas ] ; then
+ echo "\nListing CAS configuration under /etc/cas..."
+ ls -R /etc/cas
+ fi
+ echo "\nRemote debugger configured on port ${JVM_DEBUG_PORT} with suspend=${JVM_DEBUG_SUSPEND}: ${JVM_DEBUG}"
+ echo "\nJava args: ${JVM_MEM_OPTS} ${JVM_EXTRA_OPTS}"
+fi
+
+echo "\nRunning CAS @ cas.war"
+# shellcheck disable=SC2086
+exec java $JVM_EXTRA_OPTS $JVM_MEM_OPTS -jar cas.war "$@"
diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 000000000..3bc670c2c
--- /dev/null
+++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.apereo.cas.config.CasOverlayOverrideConfiguration
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 000000000..cebe337d4
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,3 @@
+# Application properties that need to be
+# embedded within the web application can be included here
+
diff --git a/system.properties b/system.properties
new file mode 100644
index 000000000..5a9b50d86
--- /dev/null
+++ b/system.properties
@@ -0,0 +1 @@
+java.runtime.version=21