diff --git a/README.md b/README.md index e8ad34cb..543173bb 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ To access your account settings, go to Account Settings in the settings cog menu To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future. -For API usage, you must provide a header with `X-API-Key` and the associated API key for that user. +For API usage, you must provide a header with `X-API-KEY` and the associated API key for that user. ## FAQ diff --git a/cucumber/features/steps/step_definitions.py b/cucumber/features/steps/step_definitions.py index 65a49fda..ae8acd2a 100644 --- a/cucumber/features/steps/step_definitions.py +++ b/cucumber/features/steps/step_definitions.py @@ -15,6 +15,10 @@ import shutil import re from PIL import Image, ImageDraw +API_HEADERS = { + 'X-API-KEY': '123456789' +} + ######### # GIVEN # ######### @@ -227,7 +231,7 @@ def save_generated_pdf(context, filename): def step_send_get_request(context, endpoint): base_url = "http://localhost:8080" full_url = f"{base_url}{endpoint}" - response = requests.get(full_url) + response = requests.get(full_url, headers=API_HEADERS) context.response = response @when('I send a GET request to "{endpoint}" with parameters') @@ -235,7 +239,7 @@ def step_send_get_request_with_params(context, endpoint): base_url = "http://localhost:8080" params = {row['parameter']: row['value'] for row in context.table} full_url = f"{base_url}{endpoint}" - response = requests.get(full_url, params=params) + response = requests.get(full_url, params=params, headers=API_HEADERS) context.response = response @when('I send the API request to the endpoint "{endpoint}"') @@ -256,7 +260,7 @@ def step_send_api_request(context, endpoint): print(f"form_data {file.name} with {mime_type}") form_data.append((key, (file.name, file, mime_type))) - response = requests.post(url, files=form_data) + response = requests.post(url, files=form_data, headers=API_HEADERS) context.response = response ######## diff --git a/exampleYmlFiles/test_cicd.yml b/exampleYmlFiles/test_cicd.yml new file mode 100644 index 00000000..144348a7 --- /dev/null +++ b/exampleYmlFiles/test_cicd.yml @@ -0,0 +1,34 @@ +services: + stirling-pdf: + container_name: Stirling-PDF-Security-Fat + image: stirlingtools/stirling-pdf:latest-fat + deploy: + resources: + limits: + memory: 4G + healthcheck: + test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + interval: 5s + timeout: 10s + retries: 16 + ports: + - 8080:8080 + volumes: + - /stirling/latest/data:/usr/share/tessdata:rw + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "true" + SECURITY_ENABLELOGIN: "true" + PUID: 1002 + PGID: 1002 + UMASK: "022" + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security + UI_APPNAMENAVBAR: Stirling-PDF Latest-fat + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + SECURITY_CUSTOMGLOBALAPIKEY: "123456789" + restart: on-failure:5 diff --git a/src/main/java/stirling/software/SPDF/config/InitialSetup.java b/src/main/java/stirling/software/SPDF/config/InitialSetup.java index 294e31ef..81279ce9 100644 --- a/src/main/java/stirling/software/SPDF/config/InitialSetup.java +++ b/src/main/java/stirling/software/SPDF/config/InitialSetup.java @@ -28,16 +28,16 @@ public class InitialSetup { @PostConstruct public void init() throws IOException { initUUIDKey(); - + initSecretKey(); - + initEnableCSRFSecurity(); - + initLegalUrls(); - + initSetAppVersion(); } - + public void initUUIDKey() throws IOException { String uuid = applicationProperties.getAutomaticallyGenerated().getUUID(); if (!GeneralUtils.isValidUUID(uuid)) { @@ -57,17 +57,17 @@ public class InitialSetup { } public void initEnableCSRFSecurity() throws IOException { - if(GeneralUtils.isVersionHigher("0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) { - Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled(); - if (!csrf) { - GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false); - GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false); - applicationProperties.getSecurity().setCsrfDisabled(false); - - } - } + if (GeneralUtils.isVersionHigher( + "0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) { + Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled(); + if (!csrf) { + GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false); + GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false); + applicationProperties.getSecurity().setCsrfDisabled(false); + } + } } - + public void initLegalUrls() throws IOException { // Initialize Terms and Conditions String termsUrl = applicationProperties.getLegal().getTermsAndConditions(); @@ -85,20 +85,19 @@ public class InitialSetup { applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl); } } - + public void initSetAppVersion() throws IOException { - - String appVersion = "0.0.0"; - Resource resource = new ClassPathResource("version.properties"); + + String appVersion = "0.0.0"; + Resource resource = new ClassPathResource("version.properties"); Properties props = new Properties(); try { props.load(resource.getInputStream()); - appVersion =props.getProperty("version"); - } catch(Exception e) { - + appVersion = props.getProperty("version"); + } catch (Exception e) { + } applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion); - GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion,false); - } - + GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false); + } } diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 7e542a00..2fcebcad 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -75,5 +75,7 @@ public class InitialSecuritySetup { userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); } + userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey()); + System.out.println(applicationProperties.getSecurity().getCustomGlobalAPIKey()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index fa3f5342..66e6f0f5 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -99,7 +99,7 @@ public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - if (applicationProperties.getSecurity().getCsrfDisabled()) { + if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { http.csrf(csrf -> csrf.disable()); } @@ -116,7 +116,7 @@ public class SecurityConfiguration { csrf -> csrf.ignoringRequestMatchers( request -> { - String apiKey = request.getHeader("X-API-Key"); + String apiKey = request.getHeader("X-API-KEY"); // If there's no API key, don't ignore CSRF // (return false) @@ -289,17 +289,17 @@ public class SecurityConfiguration { } } else { - if (!applicationProperties.getSecurity().getCsrfDisabled()) { - CookieCsrfTokenRepository cookieRepo = - CookieCsrfTokenRepository.withHttpOnlyFalse(); - CsrfTokenRequestAttributeHandler requestHandler = - new CsrfTokenRequestAttributeHandler(); - requestHandler.setCsrfRequestAttributeName(null); - http.csrf( - csrf -> - csrf.csrfTokenRepository(cookieRepo) - .csrfTokenRequestHandler(requestHandler)); - } + // if (!applicationProperties.getSecurity().getCsrfDisabled()) { + // CookieCsrfTokenRepository cookieRepo = + // CookieCsrfTokenRepository.withHttpOnlyFalse(); + // CsrfTokenRequestAttributeHandler requestHandler = + // new CsrfTokenRequestAttributeHandler(); + // requestHandler.setCsrfRequestAttributeName(null); + // http.csrf( + // csrf -> + // csrf.csrfTokenRepository(cookieRepo) + // .csrfTokenRequestHandler(requestHandler)); + // } http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); } diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index 4b62f6d2..93dff07b 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -71,7 +71,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { // Check for API key in the request headers if no authentication exists if (authentication == null || !authentication.isAuthenticated()) { - String apiKey = request.getHeader("X-API-Key"); + String apiKey = request.getHeader("X-API-KEY"); if (apiKey != null && !apiKey.trim().isEmpty()) { try { // Use API key to authenticate. This requires you to have an authentication diff --git a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java index bd7e3972..b1733494 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java @@ -59,7 +59,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { String identifier = null; // Check for API key in the request headers - String apiKey = request.getHeader("X-API-Key"); + String apiKey = request.getHeader("X-API-KEY"); if (apiKey != null && !apiKey.trim().isEmpty()) { identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames @@ -79,7 +79,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); - if (request.getHeader("X-API-Key") != null) { + if (request.getHeader("X-API-KEY") != null) { // It's an API call processRequest( userRole.getApiCallsPerDay(), diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index d7f35d38..d5fea594 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -390,6 +390,37 @@ public class UserService implements UserServiceInterface { } } + @Transactional + public void syncCustomApiUser(String customApiKey) throws IOException { + if (customApiKey == null || customApiKey.trim().length() == 0) { + return; + } + String username = "CUSTOM_API_USER"; + Optional existingUser = findByUsernameIgnoreCase(username); + + if (!existingUser.isPresent()) { + // Create new user with API role + User user = new User(); + user.setUsername(username); + user.setPassword(UUID.randomUUID().toString()); + user.setEnabled(true); + user.setFirstLogin(false); + user.setAuthenticationType(AuthenticationType.WEB); + user.setApiKey(customApiKey); + user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user)); + userRepository.save(user); + databaseBackupHelper.exportDatabase(); + } else { + // Update API key if it has changed + User user = existingUser.get(); + if (!customApiKey.equals(user.getApiKey())) { + user.setApiKey(customApiKey); + userRepository.save(user); + databaseBackupHelper.exportDatabase(); + } + } + } + @Override public long getTotalUsersCount() { return userRepository.count(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index 2e62c350..00496036 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -221,7 +221,7 @@ public class PipelineProcessor { HttpHeaders headers = new HttpHeaders(); String apiKey = getApiKeyForUser(); - headers.add("X-API-Key", apiKey); + headers.add("X-API-KEY", apiKey); headers.setContentType(MediaType.MULTIPART_FORM_DATA); // Create HttpEntity with the body and headers diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 48fe8914..703b5ee6 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -73,6 +73,7 @@ public class ApplicationProperties { private int loginAttemptCount; private long loginResetTimeMinutes; private String loginMethod = "all"; + private String customGlobalAPIKey; public Boolean isAltLogin() { return saml2.getEnabled() || oauth2.getEnabled(); diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 3f48997c..b5654c7d 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -288,10 +288,10 @@ public class GeneralUtils { public static void saveKeyToConfig(String id, String key) throws IOException { saveKeyToConfig(id, key, true); } + public static void saveKeyToConfig(String id, boolean key) throws IOException { saveKeyToConfig(id, key, true); } - public static void saveKeyToConfig(String id, String key, boolean autoGenerated) throws IOException { @@ -310,25 +310,24 @@ public class GeneralUtils { } settingsYml.save(); } - - public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated) - throws IOException { - Path path = Paths.get("configs", "settings.yml"); - - final YamlFile settingsYml = new YamlFile(path.toFile()); - DumperOptions yamlOptionssettingsYml = - ((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions(); - yamlOptionssettingsYml.setSplitLines(false); - - settingsYml.loadWithComments(); - - YamlFileWrapper writer = settingsYml.path(id).set(key); - if (autoGenerated) { - writer.comment("# Automatically Generated Settings (Do Not Edit Directly)"); - } - settingsYml.save(); - } - + + public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated) + throws IOException { + Path path = Paths.get("configs", "settings.yml"); + + final YamlFile settingsYml = new YamlFile(path.toFile()); + DumperOptions yamlOptionssettingsYml = + ((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions(); + yamlOptionssettingsYml.setSplitLines(false); + + settingsYml.loadWithComments(); + + YamlFileWrapper writer = settingsYml.path(id).set(key); + if (autoGenerated) { + writer.comment("# Automatically Generated Settings (Do Not Edit Directly)"); + } + settingsYml.save(); + } public static String generateMachineFingerprint() { try { @@ -372,7 +371,7 @@ public class GeneralUtils { return "GenericID"; } } - + public static boolean isVersionHigher(String currentVersion, String compareVersion) { if (currentVersion == null || compareVersion == null) { return false; @@ -401,5 +400,4 @@ public class GeneralUtils { // If all components so far are equal, the longer version is considered higher return current.length > compare.length; } - } diff --git a/test.sh b/test.sh index 7674c6dc..2ad25905 100644 --- a/test.sh +++ b/test.sh @@ -104,7 +104,7 @@ main() { # run_tests "Stirling-PDF-Security" "./exampleYmlFiles/docker-compose-latest-security.yml" # docker-compose -f "./exampleYmlFiles/docker-compose-latest-security.yml" down - run_tests "Stirling-PDF-Security-Fat" "./exampleYmlFiles/docker-compose-latest-fat-security.yml" + run_tests "Stirling-PDF-Security-Fat" "./exampleYmlFiles/test_cicd.yml" if [ $? -eq 0 ]; then cd cucumber if python -m behave; then