diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/FolderAccessGuard.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/FolderAccessGuard.java index 85d918d1e1..899302f5b0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/FolderAccessGuard.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/FolderAccessGuard.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; @@ -27,6 +28,7 @@ import stirling.software.proprietary.policy.model.Policy; * defended: an operator who roots an allowlist on a symlink to a sensitive location is trusted. */ @Component +@Profile("saas") public class FolderAccessGuard { public static final String FOLDER_TYPE = "folder"; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java index 1fcdbd505d..c7b10b941f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java @@ -3,6 +3,7 @@ package stirling.software.proprietary.policy.config; import java.util.List; import java.util.Objects; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; @@ -21,6 +22,7 @@ import stirling.software.proprietary.policy.model.Policy; */ @Component @RequiredArgsConstructor +@Profile("saas") public class PolicyAccessGuard { private final UserServiceInterface userService; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java index b9dc32b885..f2b751bca7 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java @@ -6,6 +6,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; @@ -64,6 +65,7 @@ import stirling.software.proprietary.policy.store.PolicyStore; @Hidden @RequiredArgsConstructor @Tag(name = "Policies", description = "Run tool pipelines on the backend") +@Profile("saas") public class PolicyController { private final PolicyRunner policyRunner; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java index 2897683a36..28951d52a2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java @@ -9,6 +9,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import org.slf4j.MDC; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; @@ -53,6 +54,7 @@ import stirling.software.proprietary.service.DownstreamEntitlementError; @Slf4j @Service @RequiredArgsConstructor +@Profile("saas") public class PolicyEngine { // Admission weight for one run. Weighted heavy: a run chains many tools and holds intermediate diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunRegistry.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunRegistry.java index edc906461e..1c88191396 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunRegistry.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunRegistry.java @@ -9,6 +9,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import jakarta.annotation.PreDestroy; @@ -28,6 +29,7 @@ import stirling.software.proprietary.policy.model.PolicyRun; */ @Slf4j @Service +@Profile("saas") public class PolicyRunRegistry { private final Map runs = new ConcurrentHashMap<>(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java index 2a59e91bb9..9aa7d284c3 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.List; import java.util.function.Consumer; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -26,6 +27,7 @@ import stirling.software.proprietary.policy.progress.PolicyProgressListener; @Slf4j @Service @RequiredArgsConstructor +@Profile("saas") public class PolicyRunner { private final PolicyEngine policyEngine; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java index c4de3b207a..6801ff38ce 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java @@ -2,6 +2,7 @@ package stirling.software.proprietary.policy.engine; import java.util.List; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -21,6 +22,7 @@ import stirling.software.proprietary.policy.trigger.PolicyTrigger; */ @Service @RequiredArgsConstructor +@Profile("saas") public class PolicyValidator { private final List triggers; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java index a6c61b6544..0990e83c18 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @@ -33,6 +34,7 @@ import stirling.software.proprietary.policy.model.PolicyInputs; @Slf4j @Service @RequiredArgsConstructor +@Profile("saas") public class FolderInputSource implements InputSource { private static final String TYPE = FolderAccessGuard.FOLDER_TYPE; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/FolderOutputSink.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/FolderOutputSink.java index e7302bd012..94a9d34a35 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/FolderOutputSink.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/FolderOutputSink.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.UUID; import org.apache.commons.io.FilenameUtils; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.MediaTypeFactory; @@ -30,6 +31,7 @@ import stirling.software.proprietary.policy.model.OutputSpec; @Slf4j @Service @RequiredArgsConstructor +@Profile("saas") public class FolderOutputSink implements PolicyOutputSink { static final String TYPE = FolderAccessGuard.FOLDER_TYPE; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/InlineOutputSink.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/InlineOutputSink.java index cb5848f527..799072aeaa 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/InlineOutputSink.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/InlineOutputSink.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.MediaTypeFactory; @@ -22,6 +23,7 @@ import stirling.software.proprietary.policy.model.OutputSpec; */ @Service @RequiredArgsConstructor +@Profile("saas") public class InlineOutputSink implements PolicyOutputSink { private static final String TYPE = "inline"; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java index 731e9f5d5d..085a97b5a7 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -18,6 +19,7 @@ import tools.jackson.databind.ObjectMapper; */ @Service @RequiredArgsConstructor +@Profile("saas") public class JpaPolicyStore implements PolicyStore { private final PolicyRepository repository; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java index ebd3795608..864160d4b6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java @@ -20,6 +20,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -45,6 +46,7 @@ import stirling.software.proprietary.policy.store.PolicyStore; @Slf4j @Service @RequiredArgsConstructor +@Profile("saas") public class FolderWatchTrigger implements PolicyTrigger { private static final String TYPE = "folder-watch"; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTriggerManager.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTriggerManager.java index 7d71138dff..c1175eeb92 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTriggerManager.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTriggerManager.java @@ -3,6 +3,7 @@ package stirling.software.proprietary.policy.trigger; import java.util.List; import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -12,6 +13,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @Service @RequiredArgsConstructor +@Profile("saas") public class PolicyTriggerManager implements SmartLifecycle { private final List triggers; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java index 65391e978a..708ca8128b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java @@ -10,6 +10,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -31,6 +32,7 @@ import tools.jackson.databind.ObjectMapper; @Slf4j @Service @RequiredArgsConstructor +@Profile("saas") public class ScheduleTrigger implements PolicyTrigger { private static final String TYPE = "schedule"; diff --git a/frontend/editor/src/core/components/tools/RightSidebar.tsx b/frontend/editor/src/core/components/tools/RightSidebar.tsx index d705709af0..795b6082ed 100644 --- a/frontend/editor/src/core/components/tools/RightSidebar.tsx +++ b/frontend/editor/src/core/components/tools/RightSidebar.tsx @@ -233,7 +233,9 @@ export default function RightSidebar() {
- + {policiesEnabled && ( + + )}
{collapsedRailItems.map(({ id, tool }) => ( { + const enabled = ["1", "true"].includes(process.env.POLICIES_E2E ?? ""); + test.skip(!enabled, "Policies are SaaS-only; set POLICIES_E2E=1 to run"); +}); + test.describe("Policy editing gate — non-admin (login on)", () => { test.use({ stubOptions: { enableLogin: true, isAdmin: false }, diff --git a/frontend/editor/src/desktop/constants/featureFlags.ts b/frontend/editor/src/desktop/constants/featureFlags.ts new file mode 100644 index 0000000000..957860b7cf --- /dev/null +++ b/frontend/editor/src/desktop/constants/featureFlags.ts @@ -0,0 +1,10 @@ +/** + * Desktop-build feature gates. Shadows `proprietary/constants/featureFlags.ts` + * (the desktop `@app/*` alias has no saas layer). Re-exports the proprietary + * flags and re-enables Policies: the desktop Policies gate additionally requires + * an active SaaS connection (see desktop PoliciesSidebar's `usePoliciesEnabled`), + * so the flag must be on for that runtime check to ever apply. + */ +export * from "@proprietary/constants/featureFlags"; + +export const POLICIES_ENABLED: boolean = true; diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx index 55e0231dbc..c05680cfb7 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx @@ -18,6 +18,10 @@ vi.mock("react-i18next", () => ({ I18nextProvider: ({ children }: { children: ReactNode }) => children, })); +// Policies ship gated SaaS-only via the build-flavor flag; these tests exercise +// the component itself, so force the flag on regardless of the test build flavor. +vi.mock("@app/constants/featureFlags", () => ({ POLICIES_ENABLED: true })); + // usePolicies derives `canConfigure` from app-config; with no AppConfigProvider // here `config` is null, which (tri-state gate) hides the edit affordances. Mock // app-config as a single-user deployment (login off) so the local operator can diff --git a/frontend/editor/src/proprietary/constants/featureFlags.ts b/frontend/editor/src/proprietary/constants/featureFlags.ts index e6dd00f6e0..599ee4ebce 100644 --- a/frontend/editor/src/proprietary/constants/featureFlags.ts +++ b/frontend/editor/src/proprietary/constants/featureFlags.ts @@ -15,8 +15,9 @@ export const WATCHED_FOLDERS_ENABLED: boolean = false; /** - * Policies — proprietary, automation-backed policy enforcement. Enabled in the - * proprietary build so it's reachable while in active development (frontend is - * mock/stub-backed; no real server yet). Core stays `false`. + * Policies — automation-backed policy enforcement. A SaaS-only feature: runs + * execute and bill through the cloud backend, so it's enabled only in the saas + * build (which overrides this to `true`) and on desktop when connected to SaaS. + * The self-hosted proprietary build and the core build keep it `false`. */ -export const POLICIES_ENABLED: boolean = true; +export const POLICIES_ENABLED: boolean = false; diff --git a/frontend/editor/src/saas/constants/featureFlags.ts b/frontend/editor/src/saas/constants/featureFlags.ts new file mode 100644 index 0000000000..4627295798 --- /dev/null +++ b/frontend/editor/src/saas/constants/featureFlags.ts @@ -0,0 +1,9 @@ +/** + * SaaS-build feature gates. Shadows `proprietary/constants/featureFlags.ts` in + * the saas build (via the `@app/*` alias). Re-exports the proprietary flags and + * overrides only those that differ for the hosted SaaS product. + */ +export * from "@proprietary/constants/featureFlags"; + +/** Policies are a SaaS-only feature — enabled here, off in proprietary/core. */ +export const POLICIES_ENABLED: boolean = true;