Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1c0dba8
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 28, 2026
f1a30df
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 28, 2026
2b5bb8c
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 28, 2026
1482cad
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 28, 2026
0457b8d
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 29, 2026
da5aeec
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 29, 2026
d5b3e5b
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 29, 2026
e5d2c53
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 29, 2026
4a972dc
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 29, 2026
bda6f67
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 29, 2026
d038e19
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 29, 2026
eddff7b
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 30, 2026
a1edc77
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 30, 2026
6cb973a
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion Apr 30, 2026
e345b2c
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 4, 2026
83ea8ef
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 4, 2026
d6a1806
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 5, 2026
683a1bf
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 5, 2026
bd621fb
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 5, 2026
80ebfcb
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 6, 2026
799ee77
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 6, 2026
e07c67f
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 6, 2026
b8895af
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 6, 2026
8e1888e
[backend] feat(multitenancy): adding builtin connectors for new tenan…
Dimfacion May 6, 2026
3cb5d25
Merge branch 'release/current' into issue/3505_add_builtin_connectors_2
corinnekrych May 13, 2026
68d23b1
[backend] fix(multi-tenancy): fix register built-in in startup path
corinnekrych May 13, 2026
edf769b
Merge branch 'release/current' into issue/3505_add_builtin_connectors_2
corinnekrych May 13, 2026
1b5be69
[backend] fix(multi-tenancy): fix duplicate item in list
corinnekrych May 13, 2026
ca39959
[backend] fix(multi-tenancy): fix duplicate item in list
corinnekrych May 13, 2026
76febd9
Merge branch 'release/current' into issue/3505_add_builtin_connectors_2
corinnekrych May 13, 2026
8253321
[backend] fix(multi-tenancy): failing test
corinnekrych May 13, 2026
50bd7be
Merge branch 'release/current' into issue/3505_add_builtin_connectors_2
corinnekrych May 13, 2026
b27bb16
[backend] fix(multi-tenancy): failing test
corinnekrych May 13, 2026
2a9809a
Merge branch 'release/current' into issue/3505_add_builtin_connectors_2
corinnekrych May 13, 2026
bb0fe2f
Merge branch 'issue/3505_add_builtin_connectors_2' of github-filigran…
corinnekrych May 13, 2026
d2a7cce
[backend] fix(multi-tenancy): failing test
corinnekrych May 13, 2026
7338c21
Merge branch 'release/current' into issue/3505_add_builtin_connectors_2
corinnekrych May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions openaev-api/src/main/java/io/openaev/aop/BypassRlsAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
* Aspect that enables RLS bypass for methods annotated with {@link BypassRls}. Sets the bypass flag
* on the current thread before execution and always clears it afterwards.
*
* <p>The {@link Order} is set to {@code Ordered.LOWEST_PRECEDENCE - 2} so that this aspect runs
* <b>before</b> {@code @Transactional} (which defaults to {@code LOWEST_PRECEDENCE}). This is
* critical because {@link io.openaev.config.TenantAwareDataSourceConfig} checks the bypass flag at
* connection-checkout time: if the flag is not yet set when the transaction opens, the connection
* is obtained under the restricted {@code openaev_app} role and PostgreSQL Row-Level Security is
* enforced, causing spurious policy violations.
*/
@Aspect
@Component
@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE - 2)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

public class BypassRlsAspect {

@Around("@annotation(io.openaev.aop.BypassRls)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io.openaev.rest.collector.service.CollectorService;
import jakarta.annotation.PostConstruct;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Service;
Expand All @@ -24,7 +26,10 @@ public void init() {
ExpectationsExpirationManagerJob job =
new ExpectationsExpirationManagerJob(
this.collectorService, this.config, this.expectationsExpirationManagerService);
this.taskScheduler.scheduleAtFixedRate(job, Duration.ofSeconds(this.config.getInterval()));
this.taskScheduler.scheduleAtFixedRate(
job,
Instant.now().plus(1, ChronoUnit.MINUTES),
Duration.ofSeconds(this.config.getInterval()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,37 @@

import io.openaev.collectors.expectations_expiration_manager.config.ExpectationsExpirationManagerConfig;
import io.openaev.collectors.expectations_expiration_manager.service.ExpectationsExpirationManagerService;
import io.openaev.integration.BuiltinTenantRegistrable;
import io.openaev.rest.collector.service.CollectorService;
import io.openaev.rest.exception.ElementNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class ExpectationsExpirationManagerJob implements Runnable {
public class ExpectationsExpirationManagerJob implements Runnable, BuiltinTenantRegistrable {
private static final String FAKE_DETECTOR_COLLECTOR_TYPE = "openaev_fake_detector";
private static final String FAKE_DETECTOR_COLLECTOR_NAME = "Expectations Expiration Manager";
private final ExpectationsExpirationManagerService fakeDetectorService;
private final CollectorService collectorService;
private final ExpectationsExpirationManagerConfig config;

@Autowired
public ExpectationsExpirationManagerJob(
CollectorService collectorService,
ExpectationsExpirationManagerConfig config,
ExpectationsExpirationManagerService fakeDetectorService) {
this.collectorService = collectorService;
this.config = config;
this.fakeDetectorService = fakeDetectorService;
}

@Override
public void registerForTenant() throws Exception {
try {
collectorService.collector(config.getId());
} catch (ElementNotFoundException e) {
collectorService.register(
config.getId(),
FAKE_DETECTOR_COLLECTOR_TYPE,
Expand All @@ -29,8 +41,6 @@ public ExpectationsExpirationManagerJob(
0,
null,
getClass().getResourceAsStream("/img/icon-fake-detector.png"));
} catch (Exception e) {
log.error("Error creating expectations expiration manager ", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package io.openaev.collectors.expectations_vulnerability_manager;

import io.openaev.integration.BuiltinTenantRegistrable;
import io.openaev.rest.collector.service.CollectorService;
import jakarta.annotation.PostConstruct;
import io.openaev.rest.exception.ElementNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
@Slf4j
public class ExpectationsVulnerabilityManagerCollector {
public class ExpectationsVulnerabilityManagerCollector implements BuiltinTenantRegistrable {

private final CollectorService collectorService;
public static final String EXPECTATIONS_VULNERABILITY_COLLECTOR_ID =
Expand All @@ -19,9 +20,11 @@ public class ExpectationsVulnerabilityManagerCollector {
public static final String EXPECTATIONS_VULNERABILITY_COLLECTOR_NAME =
"Expectations Vulnerability Manager";

@PostConstruct
public void init() {
@Override
public void registerForTenant() throws Exception {
try {
collectorService.collector(EXPECTATIONS_VULNERABILITY_COLLECTOR_ID);
} catch (ElementNotFoundException e) {
collectorService.register(
EXPECTATIONS_VULNERABILITY_COLLECTOR_ID,
EXPECTATIONS_VULNERABILITY_COLLECTOR_TYPE,
Expand All @@ -30,8 +33,6 @@ public void init() {
0,
null,
getClass().getResourceAsStream("/img/icon-expectations-vulnerability-manager.png"));
} catch (Exception e) {
log.error("Error creating expectations vulnerability manager ", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ protected List<Executor> getAllConnectors() {

@Override
protected Executor getConnectorById(String executorId) {
return executorRepository.findById(executorId).orElse(null);
return executorRepository
.findByIdAndTenantId(executorId, TenantContext.getCurrentTenant())
.orElse(null);
}

@Override
Expand Down Expand Up @@ -105,7 +107,7 @@ public Iterable<ExecutorOutput> executorsOutput(boolean isIncludeNext) {
*/
public Executor executor(String id) throws ElementNotFoundException {
return executorRepository
.findById(id)
.findByIdAndTenantId(id, TenantContext.getCurrentTenant())
.orElseThrow(() -> new ElementNotFoundException("Executor not found with id: " + id));
}

Expand Down Expand Up @@ -162,10 +164,12 @@ public Executor register(
fileService.uploadStream(EXECUTORS_IMAGES_BANNERS_BASE_PATH, type + EXT_PNG, bannerData);
}

Executor executor = executorRepository.findById(id).orElse(null);
Executor executor =
executorRepository.findByIdAndTenantId(id, TenantContext.getCurrentTenant()).orElse(null);
if (executor == null) {
executor = new Executor();
executor.setId(id);
executor.setTenant(new Tenant(TenantContext.getCurrentTenant()));
}

executor.setName(name);
Expand All @@ -179,7 +183,9 @@ public Executor register(

@Transactional
public void remove(String id) {
executorRepository.findById(id).ifPresent(executor -> executorRepository.deleteById(id));
executorRepository
.findByIdAndTenantId(id, TenantContext.getCurrentTenant())
.ifPresent(executor -> executorRepository.delete(executor));
}

/**
Expand Down
9 changes: 9 additions & 0 deletions openaev-api/src/main/java/io/openaev/helper/InjectHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
import io.openaev.execution.ExecutionContext;
import io.openaev.execution.ExecutionContextService;
import jakarta.annotation.Resource;
import jakarta.persistence.EntityManager;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import reactor.util.function.Tuple2;
Expand All @@ -41,6 +43,7 @@ public class InjectHelper {

private final InjectRepository injectRepository;
private final ExecutionContextService executionContextService;
private final EntityManager entityManager;

/**
* Retrieves the teams targeted by an inject.
Expand Down Expand Up @@ -121,6 +124,9 @@ private boolean isBeforeOrEqualsNow(Injection injection) {
* @return list of pending injects scheduled within the threshold
*/
public List<Inject> getAllPendingInjectsWithThresholdMinutes(int thresholdMinutes) {
// Disable tenant filter — called from InjectsExecutionJob which runs cross-tenant
entityManager.unwrap(Session.class).disableFilter("tenantFilter");

return this.injectRepository.findAll(
InjectSpecification.pendingInjectWithThresholdMinutes(thresholdMinutes));
}
Expand Down Expand Up @@ -153,6 +159,9 @@ private ExecutableInject toExecutableInject(Inject inject) {
*/
@Transactional
public List<ExecutableInject> getInjectsToRun() {
// Disable tenant filter — called from InjectsExecutionJob which runs cross-tenant
entityManager.unwrap(Session.class).disableFilter("tenantFilter");

// Get injects
List<Inject> injects = this.injectRepository.findAll(InjectSpecification.executable());
Stream<ExecutableInject> executableInjects =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.openaev.integration;

import io.openaev.authorisation.HttpClientFactory;
import io.openaev.service.catalog_connectors.CatalogConnectorService;
import io.openaev.service.connector_instances.ConnectorInstanceService;

/**
* Base class for built-in integration factories whose connectors must be registered for every
* tenant. Subclasses implement {@link #registerConnectorForTenant()} which contains only the
* DB-registration logic (the same calls that {@code innerStart()} does on the {@link Integration}
* side), without creating in-memory executor objects.
*
* <p>{@link ManagerFactory} discovers all {@link BuiltinTenantRegistrable} beans and calls {@link
* #registerForTenant()} once per new tenant after switching the tenant context.
*/
public abstract class BuiltinIntegrationFactory extends IntegrationFactory
implements BuiltinTenantRegistrable {

protected BuiltinIntegrationFactory(
ConnectorInstanceService connectorInstanceService,
CatalogConnectorService catalogConnectorService,
HttpClientFactory httpClientFactory) {
super(connectorInstanceService, catalogConnectorService, httpClientFactory);
}

/**
* Registers the built-in connector (injector / executor) in the <b>current</b> tenant context.
* Must be idempotent — safe to call even if the connector already exists (upsert semantics).
*/
public abstract void registerConnectorForTenant() throws Exception;

@Override
public void registerForTenant() throws Exception {
registerConnectorForTenant();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.openaev.integration;

/**
* Marker interface for any built-in component (injector, executor, collector) that must be
* registered once per tenant. Implementations are auto-discovered by {@link ManagerFactory} via
* Spring injection, so adding a new built-in component requires only implementing this interface on
* a {@code @Service} bean — no manual wiring in {@code ManagerFactory}.
*
* <p>Must be idempotent — safe to call even if the component already exists (upsert semantics).
*/
public interface BuiltinTenantRegistrable {

/** Registers this built-in component in the <b>current</b> tenant context. */
void registerForTenant() throws Exception;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package io.openaev.integration;

import static io.openaev.aop.lock.LockResourceType.MANAGER_FACTORY;
import static io.openaev.helper.StreamHelper.fromIterable;

import io.openaev.aop.lock.Lock;
import io.openaev.database.audit.TenantAssertionControl;
import io.openaev.database.model.Tenant;
import io.openaev.database.repository.TenantRepository;
import io.openaev.datapack.DataPackProcessor;
import io.openaev.multitenancy.DependenciesManager;
import io.openaev.multitenancy.DependenciesManagerException;
import io.openaev.rest.injector_contract.InjectorContractService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ManagerFactory {
public class ManagerFactory implements DependenciesManager {
private final List<IntegrationFactory> factories;
private final TenantRepository tenantRepository;
private final TenantRegistrationExecutor tenantRegistrationExecutor;

private volatile Manager manager = null;

Expand All @@ -20,6 +32,7 @@ public class ManagerFactory {
public Manager getManager() {
if (manager == null) {
try {
registerBuiltinsForAllTenants();
this.manager = new Manager(factories);
this.manager.monitorIntegrations();
} catch (Exception e) {
Expand All @@ -28,4 +41,48 @@ public Manager getManager() {
}
return this.manager;
}

/**
* Ensures built-in connectors are registered for every existing tenant. Each tenant registration
* runs in its own transaction and persistence context (via {@link TenantRegistrationExecutor}) to
* avoid JPA entity identity collisions when connector IDs are reused across tenants.
*/
private void registerBuiltinsForAllTenants() {
List<Tenant> tenants = fromIterable(tenantRepository.findAll());
TenantAssertionControl.suppress();
try {
for (Tenant tenant : tenants) {
try {
// Use isolated transaction per tenant to avoid JPA L1 cache identity collisions
// (connector IDs are reused across tenants).
tenantRegistrationExecutor.registerForTenantIsolated(tenant);
} catch (DependenciesManagerException e) {
log.error(
"Failed to register built-in connectors for tenant '{}': {}",
tenant.getName(),
e.getMessage(),
e);
}
}
} finally {
TenantAssertionControl.restore();
}
}

// -- TENANT DEPENDENCIES --

@Override
public void createDependencyForTenant(Tenant tenant) throws DependenciesManagerException {
tenantRegistrationExecutor.registerForTenant(tenant);
}

@Override
public void deleteDependencyForTenant(String tenantId) {
// Built-in connectors are tenant-scoped and deleted by CASCADE.
}

@Override
public List<Class<? extends DependenciesManager>> getPrerequisite() {
return List.of(InjectorContractService.class, DataPackProcessor.class);
}
}
Loading
Loading