mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Hardcode the Processor flag instead of a runtime property
This commit is contained in:
@@ -25,6 +25,7 @@ watchedFolders/
|
||||
# The rule above targets the app's runtime watched-folders working dir, but it
|
||||
# also matches this frontend source component dir; keep the source tracked.
|
||||
!frontend/editor/src/proprietary/components/watchedFolders/
|
||||
!frontend/editor/src/editoronly/components/watchedFolders/
|
||||
clientWebUI/
|
||||
policy-webhook-spool/
|
||||
# Scratch dir used by local fixture-regeneration runs (see
|
||||
|
||||
+2
-14
@@ -26,7 +26,6 @@ tasks:
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
|
||||
PROCESSOR_ENABLED: '{{.PROCESSOR_ENABLED}}'
|
||||
|
||||
dev:proprietary:
|
||||
desc: "Start backend dev server in proprietary mode"
|
||||
@@ -41,25 +40,14 @@ tasks:
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN | default ""}}'
|
||||
# Empty leaves the Processor on, matching a stock install.
|
||||
PROCESSOR_ENABLED: '{{.PROCESSOR_ENABLED | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
cmds:
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .PROCESSOR_ENABLED}}PROCESSOR_ENABLED={{.PROCESSOR_ENABLED}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
platforms: [windows]
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .PROCESSOR_ENABLED}}PROCESSOR_ENABLED={{.PROCESSOR_ENABLED}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
|
||||
dev:editoronly:
|
||||
desc: "Start backend dev server with the Processor off (editor-only deployment)"
|
||||
cmds:
|
||||
- task: dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.PORT}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
|
||||
PROCESSOR_ENABLED: "false"
|
||||
|
||||
dev:bundled:
|
||||
desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)"
|
||||
ignore_error: true
|
||||
|
||||
@@ -221,7 +221,7 @@ tasks:
|
||||
- '{{if .PREVIEW}}VITE_BUILD_FOR_PREVIEW=1 {{end}}npx vite build editor --mode proprietary'
|
||||
|
||||
build:editoronly:
|
||||
desc: "Build for editor-only mode (no Processor; pair with PROCESSOR_ENABLED=false)"
|
||||
desc: "Build for editor-only mode (pair with ProcessorFeature.ENABLED=false)"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vite build editor --mode editoronly
|
||||
|
||||
+4
-5
@@ -81,20 +81,19 @@ tasks:
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
|
||||
# One task, because the two halves are one deployment shape: a backend with the
|
||||
# Processor off served by an editor build that has no Processor code in it.
|
||||
# The backend half is compiled in, so this only picks the editor bundle; flip
|
||||
# ProcessorFeature.ENABLED and rebuild to move the backend with it.
|
||||
dev:editoronly:
|
||||
desc: "Start an editor-only deployment: backend with PROCESSOR_ENABLED=false + editoronly editor"
|
||||
desc: "Start the editoronly editor build (backend follows ProcessorFeature.ENABLED)"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
deps:
|
||||
- task: backend:dev:editoronly
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
# Inherited from settings.yml unless you pass SECURITY_ENABLELOGIN=true.
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
|
||||
- task: frontend:dev:editoronly
|
||||
vars:
|
||||
|
||||
+8
-9
@@ -6,18 +6,17 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
|
||||
import stirling.software.common.configuration.ProcessorFeature;
|
||||
|
||||
/**
|
||||
* Matches unless {@code processor.enabled=false}, which yields an editor-only deployment. Absent
|
||||
* the property the Processor is on, so existing installs are unaffected.
|
||||
*
|
||||
* <p>Applied to the Processor's controllers and its background/boot-work beans. Types that
|
||||
* non-Processor code injects (stores, services, JPA entities, repositories) stay ungated so the
|
||||
* context still starts with the Processor off.
|
||||
* Matches while {@link ProcessorFeature#ENABLED} is true. Applied to the Processor's controllers
|
||||
* and its background/boot-work beans; types that non-Processor code injects (stores, services, JPA
|
||||
* entities, repositories) stay ungated so the context still starts with the Processor off.
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@ConditionalOnProperty(name = "processor.enabled", havingValue = "true", matchIfMissing = true)
|
||||
@Conditional(ProcessorFeature.class)
|
||||
public @interface ConditionalOnProcessor {}
|
||||
|
||||
-17
@@ -80,7 +80,6 @@ public class ConfigInitializer {
|
||||
|
||||
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
|
||||
migrateProFeaturesKeyCasing(settingsFile, settingsTemplateFile);
|
||||
warnOnStrayProcessorFlag(settingsFile);
|
||||
|
||||
boolean changesMade =
|
||||
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
|
||||
@@ -103,22 +102,6 @@ public class ConfigInitializer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The merge below can only rewrite keys the template already defines, so a hand-added
|
||||
* processor.enabled is erased on this very boot. Nothing can carry it forward - say so loudly
|
||||
* rather than starting the Processor the admin asked to turn off.
|
||||
*/
|
||||
private void warnOnStrayProcessorFlag(YamlHelper yaml) {
|
||||
Object stray = yaml.getValueByExactKeyPath("processor", "enabled");
|
||||
if (stray != null) {
|
||||
log.warn(
|
||||
"Ignoring processor.enabled={} in settings.yml - the template merge removes"
|
||||
+ " keys it does not define. Use PROCESSOR_ENABLED or"
|
||||
+ " custom_settings.yml instead.",
|
||||
stray);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
private void migrateEnterpriseEditionToPremium(YamlHelper yaml, YamlHelper template) {
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "enabled") != null) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* The single source of truth for whether this build ships the Processor - policies, document
|
||||
* sources, classification, pipelines, triggers, integrations and the {@code /processor} portal.
|
||||
*
|
||||
* <p>Deliberately a constant rather than a property: an editor-only server is a decision made in
|
||||
* source and shipped, not something a deployment can flip. Flip {@link #ENABLED} to {@code false}
|
||||
* and rebuild to produce one.
|
||||
*
|
||||
* <p>Doubles as the Spring {@link Condition} behind {@code @ConditionalOnProcessor}, so the beans
|
||||
* and the plain-Java readers cannot disagree.
|
||||
*/
|
||||
public class ProcessorFeature implements Condition {
|
||||
|
||||
public static final boolean ENABLED = true;
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
return ENABLED;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
import stirling.software.common.model.oauth2.Provider;
|
||||
import stirling.software.common.service.SsrfProtectionService.SsrfProtectionLevel;
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.common.util.ValidationUtils;
|
||||
|
||||
@Data
|
||||
@@ -82,7 +81,6 @@ public class ApplicationProperties {
|
||||
private InternalApi internalApi = new InternalApi();
|
||||
private Cluster cluster = new Cluster();
|
||||
private Policies policies = new Policies();
|
||||
private Processor processor = new Processor();
|
||||
|
||||
@Bean
|
||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||
@@ -117,15 +115,6 @@ public class ApplicationProperties {
|
||||
return propertySource;
|
||||
}
|
||||
|
||||
/**
|
||||
* RequestUriUtils is static (called per-request from filters), so it can't be injected. Publish
|
||||
* the flag here, at bean init - long before any request can reach those filters.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void publishProcessorFlag() {
|
||||
RequestUriUtils.setProcessorEnabled(processor.isEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize fileUploadLimit from environment variables if not set in settings.yml. Supports
|
||||
* SYSTEMFILEUPLOADLIMIT (format: "100MB") and SYSTEM_MAXFILESIZE (format: "100" in MB).
|
||||
@@ -215,26 +204,6 @@ public class ApplicationProperties {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Processor {
|
||||
/**
|
||||
* Whether the Processor - policies, document sources, classification, pipelines, triggers
|
||||
* and integrations - is available on this server. On by default wherever the proprietary
|
||||
* module is present.
|
||||
*
|
||||
* <p>Turning this off yields an editor-only deployment: the Processor's beans are never
|
||||
* created, its endpoints stop being mapped, the {@code /processor} portal is unreachable,
|
||||
* and the editor hides every Processor affordance. Everything outside the Processor
|
||||
* (accounts, storage, premium, audit) is untouched, which is what separates this from
|
||||
* building the {@code core} flavour.
|
||||
*
|
||||
* <p>Deliberately absent from {@code settings.yml.template}: this is a deployment shape
|
||||
* chosen once, not a setting to browse. Set it with {@code PROCESSOR_ENABLED=false} or in
|
||||
* {@code custom_settings.yml}, which the template merge never rewrites.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Policies {
|
||||
/**
|
||||
|
||||
@@ -4,18 +4,6 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class RequestUriUtils {
|
||||
|
||||
/**
|
||||
* Mirror of {@code processor.enabled}, published by ApplicationProperties at bean init. Static
|
||||
* because every method here is, and every caller invokes them per-request from a filter, long
|
||||
* after the context has refreshed. Defaults to on so a context that never publishes (tests,
|
||||
* standalone use) behaves as it always did.
|
||||
*/
|
||||
private static volatile boolean processorEnabled = true;
|
||||
|
||||
public static void setProcessorEnabled(boolean enabled) {
|
||||
processorEnabled = enabled;
|
||||
}
|
||||
|
||||
// Share tokens are 36-char lowercase UUIDs (UUID.randomUUID().toString()); match exactly
|
||||
private static final Pattern SHARE_LINK_PATTERN =
|
||||
Pattern.compile(
|
||||
@@ -88,10 +76,7 @@ public class RequestUriUtils {
|
||||
// cookie, so the server can't authenticate the navigation itself). The
|
||||
// portal gates access via its own auth gate + RequirePortalAccess, and its
|
||||
// data APIs stay protected, so serving the shell pre-auth is safe.
|
||||
// Not on an editor-only server: there is no portal to bootstrap.
|
||||
if (processorEnabled
|
||||
&& ("/processor".equals(normalizedUri)
|
||||
|| normalizedUri.startsWith("/processor/"))) {
|
||||
if ("/processor".equals(normalizedUri) || normalizedUri.startsWith("/processor/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -151,13 +136,6 @@ public class RequestUriUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Editor-only server: the portal route-set isn't mounted, so don't serve its shell.
|
||||
if (!processorEnabled
|
||||
&& ("/processor".equals(normalizedUri)
|
||||
|| normalizedUri.startsWith("/processor/"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedUri.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
@@ -233,8 +211,7 @@ public class RequestUriUtils {
|
||||
|| trimmedUri.startsWith("/readiness")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/mobile-scanner/") // Mobile scanner endpoints (no auth)
|
||||
// Policy webhook receiver; the controller only exists with the Processor on.
|
||||
|| (processorEnabled && trimmedUri.startsWith("/api/v1/webhooks/"))
|
||||
|| trimmedUri.startsWith("/api/v1/webhooks/")
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
// Workflow participant endpoints - access controlled by share tokens, not login
|
||||
|| trimmedUri.startsWith("/api/v1/workflow/participant/")
|
||||
|
||||
@@ -88,34 +88,6 @@ class RequestUriUtilsTest {
|
||||
assertTrue(RequestUriUtils.isStaticResource("/app", "/app/processor"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessorOff_portalShellIsNotServed() {
|
||||
// Editor-only server: /processor must not be a permitAll static resource, must
|
||||
// not fall back to the SPA shell, and its webhook receiver must not be public -
|
||||
// none of those beans exist. Restored in a finally so the flag can't leak.
|
||||
try {
|
||||
RequestUriUtils.setProcessorEnabled(false);
|
||||
assertFalse(RequestUriUtils.isStaticResource("/processor"));
|
||||
assertFalse(RequestUriUtils.isStaticResource("/processor/users"));
|
||||
assertFalse(RequestUriUtils.isStaticResource("/app", "/app/processor"));
|
||||
assertFalse(RequestUriUtils.isFrontendRoute("", "/processor"));
|
||||
assertFalse(RequestUriUtils.isFrontendRoute("", "/processor/policies"));
|
||||
assertFalse(RequestUriUtils.isPublicAuthEndpoint("/api/v1/webhooks/abc", ""));
|
||||
// Editor routes are untouched.
|
||||
assertTrue(RequestUriUtils.isFrontendRoute("", "/merge"));
|
||||
assertTrue(RequestUriUtils.isStaticResource("/css/style.css"));
|
||||
assertTrue(RequestUriUtils.isPublicAuthEndpoint("/api/v1/auth/login", ""));
|
||||
} finally {
|
||||
RequestUriUtils.setProcessorEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessorOn_webhookReceiverIsPublic() {
|
||||
// Signature-verified at the controller, so it must bypass login when mounted.
|
||||
assertTrue(RequestUriUtils.isPublicAuthEndpoint("/api/v1/webhooks/abc", ""));
|
||||
}
|
||||
|
||||
// --- isFrontendRoute tests ---
|
||||
|
||||
@Test
|
||||
|
||||
@@ -176,7 +176,7 @@ def frontendBuildTask = "frontend:build:${frontendMode}"
|
||||
// editoronly strips the Processor from the bundle; the portal IS the Processor's UI.
|
||||
if (frontendMode == 'editoronly' && buildWithPortal) {
|
||||
throw new GradleException("-PfrontendMode=editoronly cannot be combined with -PbuildWithPortal=true. " +
|
||||
"An editor-only build ships no Processor UI; run the JAR with PROCESSOR_ENABLED=false to match.")
|
||||
"An editor-only build ships no Processor UI; set ProcessorFeature.ENABLED=false to match.")
|
||||
}
|
||||
|
||||
// Workspace root holds package.json and node_modules (shared across editor /
|
||||
@@ -287,7 +287,7 @@ tasks.register('npmBuild', Exec) {
|
||||
doFirst {
|
||||
println "Building editor frontend application for production (mode=${frontendMode}, VITE_API_BASE_URL=/, portal=${buildWithPortal})"
|
||||
if (frontendMode == 'editoronly') {
|
||||
println " editor-only bundle: run this JAR with PROCESSOR_ENABLED=false, or the server keeps a Processor its UI cannot reach."
|
||||
println " editor-only bundle: build this JAR with ProcessorFeature.ENABLED=false, or the server keeps a Processor its UI cannot reach."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,6 +22,7 @@ import stirling.software.SPDF.config.InitialSetup;
|
||||
import stirling.software.SPDF.controller.api.security.TimestampController;
|
||||
import stirling.software.common.annotations.api.ConfigApi;
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.configuration.ProcessorFeature;
|
||||
import stirling.software.common.configuration.interfaces.ShowAdminInterface;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.ServerCertificateServiceInterface;
|
||||
@@ -338,8 +339,7 @@ public class ConfigController {
|
||||
// Premium/Enterprise settings
|
||||
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
|
||||
|
||||
// Processor (policies, sources, classification). Off = editor-only server.
|
||||
configData.put("processorEnabled", applicationProperties.getProcessor().isEnabled());
|
||||
configData.put("processorEnabled", ProcessorFeature.ENABLED);
|
||||
|
||||
// AI Engine settings
|
||||
ApplicationProperties.AiEngine aiEngineConfig = applicationProperties.getAiEngine();
|
||||
|
||||
@@ -406,10 +406,6 @@ aiEngine:
|
||||
pdfComment: true # AI-authored PDF comments/annotations
|
||||
classify: true # Automatic document classification/labelling
|
||||
|
||||
# To run editor-only (no policies, sources, classification, routing, integrations or webhooks) set
|
||||
# PROCESSOR_ENABLED=false, or processor.enabled: false in custom_settings.yml. Do NOT put it in this
|
||||
# file - every boot rewrites settings.yml from the template and drops keys the template lacks.
|
||||
|
||||
policies:
|
||||
# Folder automations can read from and write to the directories you allow here, so treat this as a
|
||||
# security boundary. Leave allowedFolderRoots empty (default) to disable folder sources/outputs,
|
||||
|
||||
+22
-5
@@ -2,6 +2,7 @@ package stirling.software.SPDF.config;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -27,9 +28,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.common.annotations.ConditionalOnProcessor;
|
||||
|
||||
/**
|
||||
* Guards the URL surface of {@code processor.enabled=false}: every endpoint whose path belongs to
|
||||
* the Processor must sit on a class carrying {@link ConditionalOnProcessor}, so an editor-only
|
||||
* server never maps it.
|
||||
* Guards the URL surface of an editor-only build: every endpoint whose path belongs to the
|
||||
* Processor must sit on a class carrying {@link ConditionalOnProcessor}, so flipping {@code
|
||||
* ProcessorFeature.ENABLED} unmaps it.
|
||||
*
|
||||
* <p>Complements {@code ProcessorConditionalTest}, which scans the Processor's two packages. That
|
||||
* test cannot see a Processor endpoint declared elsewhere — {@code PipelineController} lives in
|
||||
@@ -83,7 +84,7 @@ class ProcessorEndpointSurfaceTest {
|
||||
assertTrue(
|
||||
offenders.isEmpty(),
|
||||
() ->
|
||||
"These endpoints stay mapped with processor.enabled=false. Add"
|
||||
"These endpoints stay mapped on an editor-only build. Add"
|
||||
+ " @ConditionalOnProcessor to the controller, or - if the endpoint"
|
||||
+ " is genuinely not part of the Processor - narrow the path lists"
|
||||
+ " in this test:\n - "
|
||||
@@ -106,6 +107,7 @@ class ProcessorEndpointSurfaceTest {
|
||||
|
||||
@Test
|
||||
void nonProcessorEndpointsInSharedNamespacesStayMapped() throws Exception {
|
||||
assumeTrue(proprietaryOnClasspath(), SKIP_REASON);
|
||||
// If a prefix ever swallowed one of these namespaces, editor features would vanish from an
|
||||
// editor-only server - the exact opposite of what the flag promises.
|
||||
Set<String> ungated = new TreeSet<>();
|
||||
@@ -134,6 +136,7 @@ class ProcessorEndpointSurfaceTest {
|
||||
|
||||
@Test
|
||||
void everyDeclaredProcessorPathIsActuallyClaimedBySomeController() throws Exception {
|
||||
assumeTrue(proprietaryOnClasspath(), SKIP_REASON);
|
||||
// A prefix nobody serves means the list has drifted from the code, and the guard above
|
||||
// would pass vacuously for that entry.
|
||||
Set<String> allPaths = new LinkedHashSet<>();
|
||||
@@ -158,6 +161,19 @@ class ProcessorEndpointSurfaceTest {
|
||||
() -> "declared Processor paths that no controller maps any more: " + unclaimed);
|
||||
}
|
||||
|
||||
private static final String SKIP_REASON =
|
||||
"core flavour builds without :proprietary, so it maps none of these paths";
|
||||
|
||||
/** app/core/build.gradle only puts :proprietary on the classpath outside the core flavour. */
|
||||
private static boolean proprietaryOnClasspath() {
|
||||
try {
|
||||
Class.forName("stirling.software.proprietary.policy.controller.PolicyController");
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isProcessorPath(String path) {
|
||||
return PROCESSOR_EXACT_PATHS.contains(path)
|
||||
|| PROCESSOR_PATH_PREFIXES.stream().anyMatch(path::startsWith);
|
||||
@@ -227,8 +243,9 @@ class ProcessorEndpointSurfaceTest {
|
||||
controllers.add(type);
|
||||
}
|
||||
}
|
||||
int floor = proprietaryOnClasspath() ? 40 : 20;
|
||||
assertTrue(
|
||||
controllers.size() > 40,
|
||||
controllers.size() > floor,
|
||||
"scan found only " + controllers.size() + " controllers - is it wired?");
|
||||
return controllers;
|
||||
}
|
||||
|
||||
+3
-7
@@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.ProcessorFeature;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.access.model.AccessPermission;
|
||||
import stirling.software.proprietary.access.model.DefaultAccessPolicy;
|
||||
@@ -36,17 +37,12 @@ public class ResourceAccessService {
|
||||
@Value("${security.portal.defaultAccess:ADMINS_AND_TEAM_LEADS}")
|
||||
private DefaultAccessPolicy portalDefaultPolicy;
|
||||
|
||||
// Initialised on: @Value lands after field init, so a directly-constructed instance must not
|
||||
// fall to Java's false and lock everyone out of the portal.
|
||||
@Value("${processor.enabled:true}")
|
||||
private boolean processorEnabled = true;
|
||||
|
||||
// ---- public checks ----
|
||||
|
||||
/** Whether the user may use the portal / processor. */
|
||||
public boolean canAccessPortal(User user) {
|
||||
// Editor-only deployment: nobody reaches the portal, not even an admin.
|
||||
if (!processorEnabled) {
|
||||
if (!ProcessorFeature.ENABLED) {
|
||||
return false;
|
||||
}
|
||||
return canUseResource(ResourceType.PORTAL, "", null, portalDefaultPolicy, user);
|
||||
@@ -59,7 +55,7 @@ public class ResourceAccessService {
|
||||
*/
|
||||
public Set<Long> usersWithPortalAccess(
|
||||
Collection<User> users, Set<Long> activeTeamLeaderUserIds) {
|
||||
if (!processorEnabled) {
|
||||
if (!ProcessorFeature.ENABLED) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<PrincipalRef> grantedPrincipals = new HashSet<>();
|
||||
|
||||
-1
@@ -24,7 +24,6 @@ import stirling.software.proprietary.security.service.ApiKeyManagementService;
|
||||
* keys. Replaces the former portal-only mock endpoint. Not gated behind an Enterprise license - API
|
||||
* keys are a core auth feature available on every self-hosted instance.
|
||||
*/
|
||||
// Serves the portal only, and an editor-only server has no portal to serve.
|
||||
@ProprietaryUiDataApi
|
||||
@ConditionalOnProcessor
|
||||
@RequiredArgsConstructor
|
||||
|
||||
-1
@@ -25,7 +25,6 @@ import stirling.software.proprietary.service.PortalDocumentsService;
|
||||
* resolved per deployment - self-hosted portal users see the whole server, SaaS users see their
|
||||
* team (see {@link PortalDocumentsScopeResolver}).
|
||||
*/
|
||||
// Serves the portal only, and an editor-only server has no portal to serve.
|
||||
@ProprietaryUiDataApi
|
||||
@ConditionalOnProcessor
|
||||
@RequiredArgsConstructor
|
||||
|
||||
-1
@@ -18,7 +18,6 @@ import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
import stirling.software.proprietary.service.PortalInfraAuditService;
|
||||
|
||||
/** Serves the Infrastructure → Audit tab from real audit data, scoped and cached per caller. */
|
||||
// Serves the portal only, and an editor-only server has no portal to serve.
|
||||
@ProprietaryUiDataApi
|
||||
@ConditionalOnProcessor
|
||||
@RequiredArgsConstructor
|
||||
|
||||
+25
-27
@@ -18,17 +18,18 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import stirling.software.common.annotations.ConditionalOnProcessor;
|
||||
import stirling.software.common.configuration.ProcessorFeature;
|
||||
|
||||
/**
|
||||
* Guards {@code processor.enabled}: every component under the Processor's two packages must carry
|
||||
* {@link ConditionalOnProcessor}, so an editor-only server starts none of them.
|
||||
* Guards {@link ProcessorFeature#ENABLED}: every component under the Processor's two packages must
|
||||
* carry {@link ConditionalOnProcessor}, so an editor-only server starts none of them.
|
||||
*
|
||||
* <p>This scans rather than hardcoding a list, so a component added later fails here instead of
|
||||
* silently shipping on a server that asked for no Processor. Anything that genuinely must survive
|
||||
@@ -76,30 +77,31 @@ class ProcessorConditionalTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void theGateReadsProcessorEnabled() {
|
||||
void theGateReadsTheCompileTimeConstant() {
|
||||
// The annotation is the single definition of the flag - assert its wiring directly,
|
||||
// since every other test here only asserts the annotation is present.
|
||||
ConditionalOnProperty conditional =
|
||||
ConditionalOnProcessor.class.getAnnotation(ConditionalOnProperty.class);
|
||||
assertNotNull(conditional, "@ConditionalOnProcessor must be a @ConditionalOnProperty");
|
||||
assertTrue(
|
||||
Arrays.asList(conditional.name()).contains("processor.enabled")
|
||||
|| Arrays.asList(conditional.value()).contains("processor.enabled"),
|
||||
"@ConditionalOnProcessor must gate on processor.enabled");
|
||||
assertEquals("true", conditional.havingValue(), "must require processor.enabled=true");
|
||||
assertTrue(conditional.matchIfMissing(), "the Processor must stay on by default");
|
||||
Conditional conditional = ConditionalOnProcessor.class.getAnnotation(Conditional.class);
|
||||
assertNotNull(conditional, "@ConditionalOnProcessor must be a @Conditional");
|
||||
assertEquals(
|
||||
List.of(ProcessorFeature.class),
|
||||
Arrays.asList(conditional.value()),
|
||||
"@ConditionalOnProcessor must be driven by ProcessorFeature");
|
||||
assertEquals(
|
||||
ProcessorFeature.ENABLED,
|
||||
new ProcessorFeature().matches(null, null),
|
||||
"the condition must report exactly what the constant says");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stacksWithAnotherConditionalOnProperty() {
|
||||
void stacksWithAnotherCondition() {
|
||||
// TelegramPipelineBot carries both this gate and its own @ConditionalOnProperty. Two
|
||||
// @ConditionalOnProperty sets on one class is subtle enough to prove rather than assume:
|
||||
// both must have to pass, not just the last one read.
|
||||
assertTrue(hasBean("feature.enabled=true", "processor.enabled=true"), "both on -> present");
|
||||
assertTrue(
|
||||
!hasBean("feature.enabled=true", "processor.enabled=false"),
|
||||
"processor off must veto even with the feature on");
|
||||
assertTrue(!hasBean("processor.enabled=true"), "the other condition must still apply");
|
||||
// conditions on one class is subtle enough to prove rather than assume: both must pass.
|
||||
assertEquals(
|
||||
ProcessorFeature.ENABLED,
|
||||
hasBean("feature.enabled=true"),
|
||||
"with the feature on, presence must track the constant");
|
||||
assertTrue(!hasBean("feature.enabled=false"), "the other condition must still apply");
|
||||
assertTrue(!hasBean(), "an absent property must still veto");
|
||||
}
|
||||
|
||||
/** By type, not name: a nested @Configuration gets an outer-class-qualified bean name. */
|
||||
@@ -182,13 +184,9 @@ class ProcessorConditionalTest {
|
||||
return source.getLocation().getPath().replace('\\', '/').contains("/classes/java/test");
|
||||
}
|
||||
|
||||
/** processor.enabled=true, else the scanner's condition evaluator hides the gated classes. */
|
||||
/** The scanner evaluates conditions; ProcessorFeature answers from the constant. */
|
||||
private static StandardEnvironment environmentWithProcessorOn() {
|
||||
StandardEnvironment environment = new StandardEnvironment();
|
||||
environment
|
||||
.getPropertySources()
|
||||
.addFirst(new MapPropertySource("test", Map.of("processor.enabled", "true")));
|
||||
return environment;
|
||||
return new StandardEnvironment();
|
||||
}
|
||||
|
||||
private static Class<?> loadClass(String name) {
|
||||
|
||||
@@ -47,8 +47,8 @@ ENV STIRLING_FLAVOR=${STIRLING_FLAVOR}
|
||||
# Embed the admin portal app at /portal. Set true by the deploy workflow when the
|
||||
# portal or AI layers change; defaults false so normal builds skip the extra app.
|
||||
ARG BUILD_PORTAL=false
|
||||
# Vite build mode. Empty keeps the flavour-derived default; "editoronly" builds an
|
||||
# editor with no Processor code, which must be run with PROCESSOR_ENABLED=false.
|
||||
# Vite build mode. Empty keeps the flavour-derived default; "editoronly" drops the
|
||||
# Processor from the bundle and pairs with ProcessorFeature.ENABLED=false.
|
||||
ARG FRONTEND_MODE=
|
||||
|
||||
# Bundle only the JPDFium native for this image's target arch.
|
||||
|
||||
@@ -9,7 +9,9 @@ export const DEFAULT_APP_CONFIG: AppConfig = { enableLogin: true };
|
||||
|
||||
/** Earliest point the Processor flag is known — feed the non-hook snapshot. */
|
||||
function publishConfig(config: AppConfig): AppConfig {
|
||||
setProcessorEnabled(config.processorEnabled === true);
|
||||
// Only an explicit false turns it off: the 401 fallback carries no flag, and
|
||||
// reading that as "off" would silently stop policy export enforcement.
|
||||
setProcessorEnabled(config.processorEnabled !== false);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// Watched folders are driven by the Processor; this build ships none.
|
||||
export default function WatchedFoldersRegistration() {
|
||||
return null;
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
# Editor-only deployment: no Processor, no portal.
|
||||
#
|
||||
# The three controls have to agree or the result is incoherent, so they are set
|
||||
# together here and nowhere else:
|
||||
# PROCESSOR_ENABLED=false backend - the Processor's beans and endpoints
|
||||
# FRONTEND_MODE=editoronly build - the editor's Processor modules
|
||||
# BUILD_PORTAL=false build - the /processor portal chunk
|
||||
# Requires ProcessorFeature.ENABLED=false in source - the backend half is compiled
|
||||
# in, not configured. The two build args below drop the matching front-end code:
|
||||
# FRONTEND_MODE=editoronly the editor's Processor modules
|
||||
# BUILD_PORTAL=false the /processor portal chunk
|
||||
#
|
||||
# docker compose -f testing/compose/docker-compose-editor-only.yml up --build
|
||||
#
|
||||
# Add `--profile control` to also bring up a stock Processor-on server on :8080.
|
||||
# Absence proves nothing on its own - the control is what makes the editor-only
|
||||
# server's missing UI and 404s mean something. It is a second full image build.
|
||||
# There is no companion Processor-on service here: the flag is compiled in, so one
|
||||
# source tree yields one shape. Compare against a normal build of main instead.
|
||||
services:
|
||||
stirling-editor-only:
|
||||
build:
|
||||
@@ -37,8 +35,6 @@ services:
|
||||
- ../../stirling/editor-only/config:/configs:rw
|
||||
- ../../stirling/editor-only/logs:/logs:rw
|
||||
environment:
|
||||
# The flag under test. Everything else here is ordinary setup.
|
||||
PROCESSOR_ENABLED: "false"
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
DOCKER_ENABLE_SECURITY: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
@@ -53,47 +49,6 @@ services:
|
||||
networks:
|
||||
- stirling-network
|
||||
|
||||
# Positive control: the same image build with nothing turned off, so the
|
||||
# editor-only server above can be diffed against a server that has it all.
|
||||
stirling-processor-control:
|
||||
profiles: ["control"]
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/embedded/Dockerfile
|
||||
args:
|
||||
BUILD_PORTAL: "true"
|
||||
container_name: Stirling-PDF-Processor-Control
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ../../stirling/processor-control/data:/usr/share/tessdata:rw
|
||||
- ../../stirling/processor-control/config:/configs:rw
|
||||
- ../../stirling/processor-control/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
DOCKER_ENABLE_SECURITY: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "admin"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "stirling"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: Stirling-PDF
|
||||
UI_HOMEDESCRIPTION: Processor-on control for the editor-only comparison
|
||||
UI_APPNAMENAVBAR: Stirling-PDF Processor
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
networks:
|
||||
- stirling-network
|
||||
|
||||
networks:
|
||||
stirling-network:
|
||||
driver: bridge
|
||||
|
||||
Reference in New Issue
Block a user