mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-03-30 00:17:49 +01:00
introduces custom settings file (#1158)
* Introducing a custom settings file * formats * chnages * Update README.md
This commit is contained in:
parent
fbbc71d7e6
commit
890163053b
README.md
src/main
java/stirling/software/SPDF
resources
@ -244,6 +244,8 @@ metrics:
|
|||||||
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
|
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is an additional config file ``/configs/custom_settings.yml`` were users familiar with java and spring application.properties can input their own settings on-top of Stirling-PDFs existing ones
|
||||||
|
|
||||||
### Extra notes
|
### Extra notes
|
||||||
|
|
||||||
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
|
@ -5,6 +5,8 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -65,14 +67,36 @@ public class SPdfApplication {
|
|||||||
|
|
||||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
|
// stirling pdf settings file
|
||||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||||
app.setDefaultProperties(
|
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
||||||
Collections.singletonMap(
|
|
||||||
"spring.config.additional-location", "file:configs/settings.yml"));
|
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom javs settings file
|
||||||
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
|
String existing = propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
|
if (!existing.isEmpty()) {
|
||||||
|
existing += ",";
|
||||||
|
}
|
||||||
|
propertyFiles.put(
|
||||||
|
"spring.config.additional-location",
|
||||||
|
existing + "file:configs/custom_settings.yml");
|
||||||
|
} else {
|
||||||
|
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!propertyFiles.isEmpty()) {
|
||||||
|
app.setDefaultProperties(
|
||||||
|
Collections.singletonMap(
|
||||||
|
"spring.config.additional-location",
|
||||||
|
propertyFiles.get("spring.config.additional-location")));
|
||||||
|
}
|
||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.regex.Matcher;
|
||||||
import java.util.stream.Collectors;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
@ -26,12 +26,12 @@ public class ConfigInitializer
|
|||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
try {
|
try {
|
||||||
ensureConfigExists();
|
ensureConfigExists();
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to initialize application configuration", e);
|
throw new RuntimeException("Failed to initialize application configuration", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ensureConfigExists() throws IOException {
|
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||||
// Define the path to the external config directory
|
// Define the path to the external config directory
|
||||||
Path destPath = Paths.get("configs", "settings.yml");
|
Path destPath = Paths.get("configs", "settings.yml");
|
||||||
|
|
||||||
@ -51,170 +51,132 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If user file exists, we need to merge it with the template from the classpath
|
Path templatePath =
|
||||||
List<String> templateLines;
|
Paths.get(
|
||||||
try (InputStream in =
|
getClass()
|
||||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
.getClassLoader()
|
||||||
templateLines =
|
.getResource("settings.yml.template")
|
||||||
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
.toURI());
|
||||||
.lines()
|
Path userPath = Paths.get("configs", "settings.yml");
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
List<String> templateLines = Files.readAllLines(templatePath);
|
||||||
|
List<String> userLines =
|
||||||
|
Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>();
|
||||||
|
|
||||||
|
Map<String, String> templateEntries = extractEntries(templateLines);
|
||||||
|
Map<String, String> userEntries = extractEntries(userLines);
|
||||||
|
|
||||||
|
List<String> mergedLines = mergeConfigs(templateLines, templateEntries, userEntries);
|
||||||
|
mergedLines = cleanInvalidYamlEntries(mergedLines);
|
||||||
|
Files.write(userPath, mergedLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeYamlFiles(templateLines, destPath, destPath);
|
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
||||||
|
if (!Files.exists(customSettingsPath)) {
|
||||||
|
Files.createFile(customSettingsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
|
private static Map<String, String> extractEntries(List<String> lines) {
|
||||||
throws IOException {
|
Map<String, String> entries = new HashMap<>();
|
||||||
List<String> userLines = Files.readAllLines(userFilePath);
|
String keyRegex = "^\\s*(\\w+)\\s*:\\s*(.*)"; // Capture key and value
|
||||||
|
Pattern pattern = Pattern.compile(keyRegex);
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
Matcher matcher = pattern.matcher(line);
|
||||||
|
if (matcher.find() && !line.trim().startsWith("#")) {
|
||||||
|
String key = matcher.group(1).trim();
|
||||||
|
String value = matcher.group(2).trim(); // Capture the value directly
|
||||||
|
entries.put(key, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static List<String> mergeConfigs(
|
||||||
|
List<String> templateLines,
|
||||||
|
Map<String, String> templateEntries,
|
||||||
|
Map<String, String> userEntries) {
|
||||||
List<String> mergedLines = new ArrayList<>();
|
List<String> mergedLines = new ArrayList<>();
|
||||||
boolean insideAutoGenerated = false;
|
Set<String> handledKeys = new HashSet<>();
|
||||||
boolean beforeFirstKey = true;
|
|
||||||
|
|
||||||
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
|
for (String line : templateLines) {
|
||||||
Function<String, String> extractKey =
|
String cleanLine = line.split("#")[0].trim();
|
||||||
line -> {
|
if (!cleanLine.isEmpty() && cleanLine.contains(":")) {
|
||||||
String[] parts = line.split(":");
|
String key = cleanLine.split(":")[0].trim();
|
||||||
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
|
if (userEntries.containsKey(key)) {
|
||||||
};
|
// Always use user's entry if exists
|
||||||
|
mergedLines.add(userEntries.get(key));
|
||||||
|
handledKeys.add(key);
|
||||||
|
} else {
|
||||||
|
// Use template's entry if no user entry
|
||||||
|
mergedLines.add(line);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add comments and other lines directly
|
||||||
|
mergedLines.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Function<String, Integer> getIndentationLevel =
|
// Add user entries not present in the template at the end
|
||||||
line -> {
|
for (String key : userEntries.keySet()) {
|
||||||
|
if (!handledKeys.contains(key)) {
|
||||||
|
mergedLines.add(userEntries.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static List<String> cleanInvalidYamlEntries(List<String> lines) {
|
||||||
|
List<String> cleanedLines = new ArrayList<>();
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
String line = lines.get(i);
|
||||||
|
String trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// Ignore commented lines and lines that don't look like key-only entries
|
||||||
|
if (trimmedLine.startsWith("#")
|
||||||
|
|| !trimmedLine.endsWith(":")
|
||||||
|
|| trimmedLine.contains(" ")) {
|
||||||
|
cleanedLines.add(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For potential key-only lines, check the next line to determine context
|
||||||
|
if (isKeyWithoutChildrenOrValue(i, lines)) {
|
||||||
|
// Skip adding the current line since it's a key without any following value or
|
||||||
|
// children
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedLines.add(line);
|
||||||
|
}
|
||||||
|
return cleanedLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isKeyWithoutChildrenOrValue(int currentIndex, List<String> lines) {
|
||||||
|
if (currentIndex + 1 < lines.size()) {
|
||||||
|
String currentLine = lines.get(currentIndex);
|
||||||
|
String nextLine = lines.get(currentIndex + 1);
|
||||||
|
int currentIndentation = getIndentationLevel(currentLine);
|
||||||
|
int nextIndentation = getIndentationLevel(nextLine);
|
||||||
|
|
||||||
|
// If the next line is less or equally indented, it's not a child or value
|
||||||
|
return nextIndentation <= currentIndentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's the last line, then it definitely has no children or value
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getIndentationLevel(String line) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (char ch : line.toCharArray()) {
|
for (char ch : line.toCharArray()) {
|
||||||
if (ch == ' ') count++;
|
if (ch == ' ') count++;
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
};
|
|
||||||
|
|
||||||
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
for (String line : templateLines) {
|
|
||||||
String key = extractKey.apply(line);
|
|
||||||
|
|
||||||
if ("AutomaticallyGenerated:".equalsIgnoreCase(line.trim())) {
|
|
||||||
insideAutoGenerated = true;
|
|
||||||
mergedLines.add(line);
|
|
||||||
continue;
|
|
||||||
} else if (insideAutoGenerated && line.trim().isEmpty()) {
|
|
||||||
insideAutoGenerated = false;
|
|
||||||
mergedLines.add(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
|
|
||||||
// Handle top comments and empty lines before the first key.
|
|
||||||
mergedLines.add(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!key.isEmpty()) beforeFirstKey = false;
|
|
||||||
|
|
||||||
if (userKeys.contains(key)) {
|
|
||||||
// If user has any version (commented or uncommented) of this key, skip the
|
|
||||||
// template line
|
|
||||||
Optional<String> userValue =
|
|
||||||
userLines.stream()
|
|
||||||
.filter(
|
|
||||||
l ->
|
|
||||||
extractKey.apply(l).equalsIgnoreCase(key)
|
|
||||||
&& !isCommented.apply(l))
|
|
||||||
.findFirst();
|
|
||||||
if (userValue.isPresent()) mergedLines.add(userValue.get());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
|
|
||||||
mergedLines.add(
|
|
||||||
line); // If line is commented, empty or key not present in user's file,
|
|
||||||
// retain the
|
|
||||||
// template line
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any additional uncommented user lines that are not present in the
|
|
||||||
// template
|
|
||||||
for (String userLine : userLines) {
|
|
||||||
String userKey = extractKey.apply(userLine);
|
|
||||||
boolean isPresentInTemplate =
|
|
||||||
templateLines.stream()
|
|
||||||
.map(extractKey)
|
|
||||||
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
|
|
||||||
if (!isPresentInTemplate && !isCommented.apply(userLine)) {
|
|
||||||
if (!childOfTemplateEntry(
|
|
||||||
isCommented,
|
|
||||||
extractKey,
|
|
||||||
getIndentationLevel,
|
|
||||||
userLines,
|
|
||||||
userLine,
|
|
||||||
templateLines)) {
|
|
||||||
// check if userLine is a child of a entry within templateLines or not, if child
|
|
||||||
// of parent in templateLines then dont add to mergedLines, if anything else
|
|
||||||
// then add
|
|
||||||
mergedLines.add(userLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// New method to check if a userLine is a child of an entry in templateLines
|
|
||||||
boolean childOfTemplateEntry(
|
|
||||||
Function<String, Boolean> isCommented,
|
|
||||||
Function<String, String> extractKey,
|
|
||||||
Function<String, Integer> getIndentationLevel,
|
|
||||||
List<String> userLines,
|
|
||||||
String userLine,
|
|
||||||
List<String> templateLines) {
|
|
||||||
String userKey = extractKey.apply(userLine).trim();
|
|
||||||
int userIndentation = getIndentationLevel.apply(userLine);
|
|
||||||
|
|
||||||
// Start by assuming the line is not a child of an entry in templateLines
|
|
||||||
boolean isChild = false;
|
|
||||||
|
|
||||||
// Iterate backwards through userLines from the current line to find any parent
|
|
||||||
for (int i = userLines.indexOf(userLine) - 1; i >= 0; i--) {
|
|
||||||
String potentialParentLine = userLines.get(i);
|
|
||||||
int parentIndentation = getIndentationLevel.apply(potentialParentLine);
|
|
||||||
|
|
||||||
// Check if we've reached a potential parent based on indentation
|
|
||||||
if (parentIndentation < userIndentation) {
|
|
||||||
String parentKey = extractKey.apply(potentialParentLine).trim();
|
|
||||||
|
|
||||||
// Now, check if this potential parent or any of its parents exist in templateLines
|
|
||||||
boolean parentExistsInTemplate =
|
|
||||||
templateLines.stream()
|
|
||||||
.filter(line -> !isCommented.apply(line)) // Skip commented lines
|
|
||||||
.anyMatch(
|
|
||||||
templateLine -> {
|
|
||||||
String templateKey =
|
|
||||||
extractKey.apply(templateLine).trim();
|
|
||||||
return parentKey.equalsIgnoreCase(templateKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!parentExistsInTemplate) {
|
|
||||||
// If the parent does not exist in template, check the next level parent
|
|
||||||
userIndentation =
|
|
||||||
parentIndentation; // Update userIndentation to the parent's indentation
|
|
||||||
// for next iteration
|
|
||||||
if (parentIndentation == 0) {
|
|
||||||
// If we've reached the top-level parent and it's not in template, the
|
|
||||||
// original line is considered not a child
|
|
||||||
isChild = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If any parent exists in template, the original line is considered a child
|
|
||||||
isChild = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isChild; // Return true if the line is not a child of any entry in templateLines
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,41 +2,37 @@ package stirling.software.SPDF.config.security;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpSession;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler
|
import jakarta.servlet.ServletException;
|
||||||
{
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
@Bean
|
@Bean
|
||||||
public SessionRegistry sessionRegistry() {
|
public SessionRegistry sessionRegistry() {
|
||||||
return new SessionRegistryImpl();
|
return new SessionRegistryImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
|
public void onLogoutSuccess(
|
||||||
{
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException {
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
String sessionId = session.getId();
|
String sessionId = session.getId();
|
||||||
sessionRegistry()
|
sessionRegistry().removeSessionInformation(sessionId);
|
||||||
.removeSessionInformation(
|
|
||||||
sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.getParameter("oauth2AutoCreateDisabled") != null)
|
if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
{
|
response.sendRedirect(
|
||||||
response.sendRedirect(request.getContextPath()+"/login?error=oauth2AutoCreateDisabled");
|
request.getContextPath() + "/login?error=oauth2AutoCreateDisabled");
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ public class InitialSecuritySetup {
|
|||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initSecretKey() throws IOException {
|
public void initSecretKey() throws IOException {
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
if (secretKey == null || secretKey.isEmpty()) {
|
if (!isValidUUID(secretKey)) {
|
||||||
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
||||||
saveKeyToConfig(secretKey);
|
saveKeyToConfig(secretKey);
|
||||||
}
|
}
|
||||||
@ -85,4 +85,16 @@ public class InitialSecuritySetup {
|
|||||||
// Write back to the file
|
// Write back to the file
|
||||||
Files.write(path, lines);
|
Files.write(path, lines);
|
||||||
}
|
}
|
||||||
|
private boolean isValidUUID(String uuid) {
|
||||||
|
if (uuid == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import java.io.IOException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import java.util.*;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -24,6 +24,8 @@ import org.springframework.security.core.userdetails.UserDetailsService;
|
|||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||||
@ -33,17 +35,15 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
|
|||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity()
|
@EnableWebSecurity()
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
@ -109,7 +109,7 @@ public class SecurityConfiguration {
|
|||||||
logout ->
|
logout ->
|
||||||
logout.logoutRequestMatcher(
|
logout.logoutRequestMatcher(
|
||||||
new AntPathRequestMatcher("/logout"))
|
new AntPathRequestMatcher("/logout"))
|
||||||
.logoutSuccessHandler(new CustomLogoutSuccessHandler()) // Use a Custom Logout Handler to handle custom error message if OAUTH2 Auto Create is disabled
|
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
|
||||||
.invalidateHttpSession(true) // Invalidate session
|
.invalidateHttpSession(true) // Invalidate session
|
||||||
.deleteCookies("JSESSIONID", "remember-me")
|
.deleteCookies("JSESSIONID", "remember-me")
|
||||||
.addLogoutHandler(
|
.addLogoutHandler(
|
||||||
@ -167,32 +167,44 @@ public class SecurityConfiguration {
|
|||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
||||||
|
|
||||||
http.oauth2Login( oauth2 -> oauth2
|
http.oauth2Login(
|
||||||
.loginPage("/oauth2")
|
oauth2 ->
|
||||||
|
oauth2.loginPage("/oauth2")
|
||||||
/*
|
/*
|
||||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||||
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||||
is set as true, else login fails with an error message advising the same.
|
is set as true, else login fails with an error message advising the same.
|
||||||
*/
|
*/
|
||||||
.successHandler(new AuthenticationSuccessHandler() {
|
.successHandler(
|
||||||
|
new AuthenticationSuccessHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
public void onAuthenticationSuccess(
|
||||||
Authentication authentication) throws ServletException , IOException{
|
HttpServletRequest request,
|
||||||
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
HttpServletResponse response,
|
||||||
if (userService.processOAuth2PostLogin(oauthUser.getAttribute("email"), applicationProperties.getSecurity().getOAUTH2().getAutoCreateUser())) {
|
Authentication authentication)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
OAuth2User oauthUser =
|
||||||
|
(OAuth2User)
|
||||||
|
authentication
|
||||||
|
.getPrincipal();
|
||||||
|
if (userService.processOAuth2PostLogin(
|
||||||
|
oauthUser.getAttribute("email"),
|
||||||
|
applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getOAUTH2()
|
||||||
|
.getAutoCreateUser())) {
|
||||||
response.sendRedirect("/");
|
response.sendRedirect("/");
|
||||||
}
|
} else {
|
||||||
else{
|
response.sendRedirect(
|
||||||
response.sendRedirect("/logout?oauth2AutoCreateDisabled=true");
|
"/logout?oauth2AutoCreateDisabled=true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
// Add existing Authorities from the database
|
// Add existing Authorities from the database
|
||||||
.userInfoEndpoint( userInfoEndpoint ->
|
.userInfoEndpoint(
|
||||||
userInfoEndpoint.userAuthoritiesMapper(userAuthoritiesMapper())
|
userInfoEndpoint ->
|
||||||
)
|
userInfoEndpoint.userAuthoritiesMapper(
|
||||||
);
|
userAuthoritiesMapper())));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
@ -204,13 +216,17 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
// Client Registration Repository for OAUTH2 OIDC Login
|
// Client Registration Repository for OAUTH2 OIDC Login
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = "security.oauth2.enabled" , havingValue = "true", matchIfMissing = false)
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
|
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientRegistration oidcClientRegistration() {
|
private ClientRegistration oidcClientRegistration() {
|
||||||
return ClientRegistrations.fromOidcIssuerLocation(applicationProperties.getSecurity().getOAUTH2().getIssuer())
|
return ClientRegistrations.fromOidcIssuerLocation(
|
||||||
|
applicationProperties.getSecurity().getOAUTH2().getIssuer())
|
||||||
.registrationId("oidc")
|
.registrationId("oidc")
|
||||||
.clientId(applicationProperties.getSecurity().getOAUTH2().getClientId())
|
.clientId(applicationProperties.getSecurity().getOAUTH2().getClientId())
|
||||||
.clientSecret(applicationProperties.getSecurity().getOAUTH2().getClientSecret())
|
.clientSecret(applicationProperties.getSecurity().getOAUTH2().getClientSecret())
|
||||||
@ -225,25 +241,30 @@ public class SecurityConfiguration {
|
|||||||
This is required for the internal; 'hasRole()' function to give out the correct role.
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = "security.oauth2.enabled" , havingValue = "true", matchIfMissing = false)
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||||
return (authorities) -> {
|
return (authorities) -> {
|
||||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||||
|
|
||||||
authorities.forEach(authority -> {
|
authorities.forEach(
|
||||||
|
authority -> {
|
||||||
// Add existing OAUTH2 Authorities
|
// Add existing OAUTH2 Authorities
|
||||||
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
||||||
|
|
||||||
// Add Authorities from database for existing user, if user is present.
|
// Add Authorities from database for existing user, if user is present.
|
||||||
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase((String)oauth2Auth.getAttributes().get("email"));
|
Optional<User> userOpt =
|
||||||
|
userService.findByUsernameIgnoreCase(
|
||||||
|
(String) oauth2Auth.getAttributes().get("email"));
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (user != null){
|
if (user != null) {
|
||||||
mappedAuthorities.add(new SimpleGrantedAuthority(
|
mappedAuthorities.add(
|
||||||
userService
|
new SimpleGrantedAuthority(
|
||||||
.findRole(user)
|
userService.findRole(user).getAuthority()));
|
||||||
.getAuthority()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
user.setFirstLogin(false);
|
user.setFirstLogin(false);
|
||||||
user.addAuthority(new Authority( Role.USER.getRoleId(), user));
|
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@ -39,7 +38,8 @@ public class AccountWebController {
|
|||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
model.addAttribute(
|
||||||
|
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
||||||
|
|
||||||
model.addAttribute("currentPage", "login");
|
model.addAttribute("currentPage", "login");
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ public class ApplicationProperties {
|
|||||||
+ ", clientId="
|
+ ", clientId="
|
||||||
+ clientId
|
+ clientId
|
||||||
+ ", clientSecret="
|
+ ", clientSecret="
|
||||||
+ (clientSecret!= null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||||
+ ", autoCreateUser="
|
+ ", autoCreateUser="
|
||||||
+ autoCreateUser
|
+ autoCreateUser
|
||||||
+ "]";
|
+ "]";
|
||||||
|
@ -11,5 +11,4 @@ public interface AuthorityRepository extends JpaRepository<Authority, Long> {
|
|||||||
Set<Authority> findByUser_Username(String username);
|
Set<Authority> findByUser_Username(String username);
|
||||||
|
|
||||||
Authority findByUserId(long user_id);
|
Authority findByUserId(long user_id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ system:
|
|||||||
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
|
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
|
||||||
showUpdate: true # see when a new update is available
|
showUpdate: true # see when a new update is available
|
||||||
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
||||||
customHTMLFiles: false # Enable to have files placed in /customFiles/templates override the existing template html files
|
|
||||||
|
|
||||||
#ui:
|
#ui:
|
||||||
# appName: exampleAppName # Application's visible name
|
# appName: exampleAppName # Application's visible name
|
||||||
@ -33,3 +32,7 @@ endpoints:
|
|||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
|
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
|
||||||
|
|
||||||
|
# Automatically Generated Settings (Do Not Edit Directly)
|
||||||
|
AutomaticallyGenerated:
|
||||||
|
key: example
|
Loading…
Reference in New Issue
Block a user