feat(outbound): service with uniqueness + narrow-envs + delete-if-referenced guards
rulesReferencing() is stubbed; wired to AlertRuleRepository in Plan 02. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
package com.cameleer.server.app.outbound;
|
||||||
|
|
||||||
|
import com.cameleer.server.core.outbound.OutboundConnection;
|
||||||
|
import com.cameleer.server.core.outbound.OutboundConnectionRepository;
|
||||||
|
import com.cameleer.server.core.outbound.OutboundConnectionService;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class OutboundConnectionServiceImpl implements OutboundConnectionService {
|
||||||
|
|
||||||
|
private final OutboundConnectionRepository repo;
|
||||||
|
private final String tenantId;
|
||||||
|
|
||||||
|
public OutboundConnectionServiceImpl(OutboundConnectionRepository repo, String tenantId) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutboundConnection create(OutboundConnection draft, String actingUserId) {
|
||||||
|
assertNameUnique(draft.name(), null);
|
||||||
|
OutboundConnection c = new OutboundConnection(
|
||||||
|
UUID.randomUUID(), tenantId, draft.name(), draft.description(),
|
||||||
|
draft.url(), draft.method(), draft.defaultHeaders(), draft.defaultBodyTmpl(),
|
||||||
|
draft.tlsTrustMode(), draft.tlsCaPemPaths(),
|
||||||
|
draft.hmacSecretCiphertext(),
|
||||||
|
draft.auth(), draft.allowedEnvironmentIds(),
|
||||||
|
Instant.now(), actingUserId, Instant.now(), actingUserId);
|
||||||
|
return repo.save(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutboundConnection update(UUID id, OutboundConnection draft, String actingUserId) {
|
||||||
|
OutboundConnection existing = repo.findById(tenantId, id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!existing.name().equals(draft.name())) {
|
||||||
|
assertNameUnique(draft.name(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrowing allowed-envs guard: if the new draft restricts to a non-empty set of envs,
|
||||||
|
// find any envs that existed before but are absent in the draft.
|
||||||
|
// Skip entirely if either side is empty (empty = "allowed in all envs").
|
||||||
|
if (!existing.allowedEnvironmentIds().isEmpty() && !draft.allowedEnvironmentIds().isEmpty()) {
|
||||||
|
List<UUID> removed = existing.allowedEnvironmentIds().stream()
|
||||||
|
.filter(e -> !draft.allowedEnvironmentIds().contains(e))
|
||||||
|
.toList();
|
||||||
|
if (!removed.isEmpty()) {
|
||||||
|
List<UUID> refs = rulesReferencing(id); // Plan 01 stub
|
||||||
|
if (!refs.isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"Narrowing allowed environments while rules still reference this connection in removed envs: " + refs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutboundConnection updated = new OutboundConnection(
|
||||||
|
id, tenantId, draft.name(), draft.description(), draft.url(), draft.method(),
|
||||||
|
draft.defaultHeaders(), draft.defaultBodyTmpl(),
|
||||||
|
draft.tlsTrustMode(), draft.tlsCaPemPaths(),
|
||||||
|
// Retain existing secret if the draft omitted one (null = leave unchanged).
|
||||||
|
draft.hmacSecretCiphertext() != null ? draft.hmacSecretCiphertext() : existing.hmacSecretCiphertext(),
|
||||||
|
draft.auth(), draft.allowedEnvironmentIds(),
|
||||||
|
existing.createdAt(), existing.createdBy(), Instant.now(), actingUserId);
|
||||||
|
return repo.save(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(UUID id, String actingUserId) {
|
||||||
|
List<UUID> refs = rulesReferencing(id); // Plan 01 stub
|
||||||
|
if (!refs.isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"Outbound connection is referenced by rules: " + refs);
|
||||||
|
}
|
||||||
|
repo.delete(tenantId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutboundConnection get(UUID id) {
|
||||||
|
return repo.findById(tenantId, id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OutboundConnection> list() {
|
||||||
|
return repo.listByTenant(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UUID> rulesReferencing(UUID id) {
|
||||||
|
// Plan 01 stub. Plan 02 will wire this to AlertRuleRepository.
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertNameUnique(String name, UUID excludingId) {
|
||||||
|
repo.findByName(tenantId, name).ifPresent(c -> {
|
||||||
|
if (!c.id().equals(excludingId)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Connection name already exists: " + name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.cameleer.server.app.outbound.config;
|
package com.cameleer.server.app.outbound.config;
|
||||||
|
|
||||||
|
import com.cameleer.server.app.outbound.OutboundConnectionServiceImpl;
|
||||||
import com.cameleer.server.app.outbound.crypto.SecretCipher;
|
import com.cameleer.server.app.outbound.crypto.SecretCipher;
|
||||||
import com.cameleer.server.app.outbound.storage.PostgresOutboundConnectionRepository;
|
import com.cameleer.server.app.outbound.storage.PostgresOutboundConnectionRepository;
|
||||||
import com.cameleer.server.core.outbound.OutboundConnectionRepository;
|
import com.cameleer.server.core.outbound.OutboundConnectionRepository;
|
||||||
|
import com.cameleer.server.core.outbound.OutboundConnectionService;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -23,4 +25,11 @@ public class OutboundBeanConfig {
|
|||||||
String jwtSecret) {
|
String jwtSecret) {
|
||||||
return new SecretCipher(jwtSecret);
|
return new SecretCipher(jwtSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OutboundConnectionService outboundConnectionService(
|
||||||
|
OutboundConnectionRepository repo,
|
||||||
|
@Value("${cameleer.server.tenant.id:default}") String tenantId) {
|
||||||
|
return new OutboundConnectionServiceImpl(repo, tenantId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user