Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8263084
test(harness): add full-context @SpringBootTest integration harness
krusche Jul 3, 2026
d20958d
test(scoping): guard that GET /api/environments* is scoped per reposi…
krusche Jul 3, 2026
16234a7
refactor(env): explicit per-repository filtering for environment reads
krusche Jul 3, 2026
1557a7c
refactor(branch): explicit per-repository filtering for GET /api/bran…
krusche Jul 3, 2026
e6ab21f
refactor(env,branch): never findAll for tenant reads — empty on missi…
krusche Jul 3, 2026
0738587
refactor(workflow): explicit per-repository filtering for workflow reads
krusche Jul 3, 2026
e11d967
refactor(deployment): explicit per-repository filtering for deploymen…
krusche Jul 3, 2026
b9233f8
refactor(workflow-run): remove dead unscoped getAllWorkflowRuns
krusche Jul 3, 2026
342ad2a
refactor(pull-request): explicit per-repository filtering for pull-re…
krusche Jul 3, 2026
9db96af
refactor(release-info): explicit per-repository filtering for getAllR…
krusche Jul 3, 2026
57341d6
refactor(tenancy): remove the Hibernate gitRepositoryFilter — scoping…
krusche Jul 3, 2026
fc3f484
fix(settings): stop GET /settings from persisting a settings row (wri…
krusche Jul 3, 2026
1dc0e04
refactor(tx): drop class-level @Transactional from GitHubService
krusche Jul 3, 2026
c42405b
refactor(tx): remove class-level @Transactional from read-heavy services
krusche Jul 3, 2026
5be962f
refactor(tx): remove class-level @Transactional from GitRepoSettingsS…
krusche Jul 3, 2026
b21463a
refactor: delete dead unscoped finders + empty IssueRepository
krusche Jul 4, 2026
a543030
refactor(tx): keep @Transactional only where mechanically required
krusche Jul 4, 2026
15dabfe
fix(tenancy): close cross-repo leaks surfaced by deep review
krusche Jul 4, 2026
c9de350
fix(settings): PUT /settings creates the row on absent
krusche Jul 4, 2026
ca5220e
refactor: remove dead PullRequestRepository.findAllByOrderByUpdatedAt…
krusche Jul 4, 2026
fb41e47
fix(tx): run read methods @Transactional(readOnly = true) — Postgres …
krusche Jul 4, 2026
9258efd
fix(tx): run TestResultService reads @Transactional(readOnly = true)
krusche Jul 4, 2026
db87b28
refactor(tx): disable JDBC auto-commit instead of annotating reads @T…
krusche Jul 4, 2026
677cd3e
test(tenancy): add PullRequestScopingIT (real-path cross-repo guard)
krusche Jul 4, 2026
628d45d
test(tenancy): add WorkflowRunScopingIT (real-path cross-repo guard)
krusche Jul 4, 2026
a667ee5
test(tenancy): add ReleaseInfoScopingIT (real-path cross-repo guard)
krusche Jul 4, 2026
2d8d290
test(tenancy): add TestResultScopingIT (real-path cross-repo guard)
krusche Jul 4, 2026
663d403
test(settings): add GitRepoSettingsIT (write-on-GET guard)
krusche Jul 4, 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Filter;

@Entity
@IdClass(BranchId.class)
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@Filter(name = "gitRepositoryFilter")
public class Branch {

@Id private String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

@Repository
public interface BranchRepository extends JpaRepository<Branch, BranchId> {
Optional<Branch> findByName(String name);

Optional<Branch> findByNameAndRepositoryRepositoryId(String name, Long repositoryId);

void deleteByNameAndRepositoryRepositoryId(String name, Long repositoryId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.tum.cit.aet.helios.auth.AuthService;
import de.tum.cit.aet.helios.commit.CommitRepository;
import de.tum.cit.aet.helios.filters.RepositoryContext;
import de.tum.cit.aet.helios.releaseinfo.releasecandidate.ReleaseCandidate;
import de.tum.cit.aet.helios.releaseinfo.releasecandidate.ReleaseCandidateRepository;
import de.tum.cit.aet.helios.userpreference.UserPreference;
Expand All @@ -17,7 +18,6 @@
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class BranchService {

Expand All @@ -32,7 +32,11 @@ public List<BranchInfoDto> getAllBranches(String sortField, String sortDirection
? userPreferenceRepository.findByUser(authService.getUserFromGithubId())
: Optional.empty();
Comparator<BranchInfoDto> comparator = buildBranchInfoDtoComparator(sortField, sortDirection);
return branchRepository.findAll().stream()
Long repositoryId = RepositoryContext.getRepositoryId();
if (repositoryId == null) {
return List.of();
}
return branchRepository.findByRepositoryRepositoryId(repositoryId).stream()
.map((branch) -> BranchInfoDto.fromBranchAndUserPreference(branch, userPreference,
commitRepository))
.sorted(comparator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,7 @@ public WebMvcConfigurer corsConfigurer() {

@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
// Order last (Integer.MAX_VALUE == Ordered.LOWEST_PRECEDENCE) so this runs AFTER Spring's
// Open-Session-In-View interceptor. That guarantees the request's EntityManager is already
// bound when RepositoryInterceptor enables the tenant filter on it.
registry.addWebRequestInterceptor(requestInterceptor).order(Integer.MAX_VALUE);
registry.addWebRequestInterceptor(requestInterceptor);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public interface DeploymentRepository extends JpaRepository<Deployment, Long> {

List<Deployment> findByRepositoryRepositoryIdAndSha(Long repositoryId, String sha);

// Explicit per-repository scoped finders (Option B).
List<Deployment> findByRepositoryRepositoryIdOrderByCreatedAtDesc(Long repositoryId);

Optional<Deployment> findByIdAndRepositoryRepositoryId(Long id, Long repositoryId);

List<Deployment> findByRepositoryRepositoryIdAndRefOrderByCreatedAtDesc(
Long repositoryId, String ref);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import de.tum.cit.aet.helios.workflow.Workflow;
import de.tum.cit.aet.helios.workflow.WorkflowRunRepository;
import jakarta.persistence.EntityNotFoundException;
import jakarta.transaction.Transactional;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
Expand All @@ -40,7 +39,6 @@

@Log4j2
@Service
@Transactional
@RequiredArgsConstructor
public class DeploymentService {

Expand All @@ -60,22 +58,49 @@ public class DeploymentService {
private final WorkflowRunRepository workflowRunRepository;

public Optional<DeploymentDto> getDeploymentById(Long id) {
return deploymentRepository.findById(id).map(DeploymentDto::fromDeployment);
Long repositoryId = RepositoryContext.getRepositoryId();
Optional<Deployment> deployment =
repositoryId == null
? Optional.empty()
: deploymentRepository.findByIdAndRepositoryRepositoryId(id, repositoryId);
return deployment.map(DeploymentDto::fromDeployment);
}

public List<DeploymentDto> getAllDeployments() {
return deploymentRepository.findAll().stream()
Long repositoryId = RepositoryContext.getRepositoryId();
if (repositoryId == null) {
return List.of();
}
return deploymentRepository.findByRepositoryRepositoryIdOrderByCreatedAtDesc(repositoryId)
.stream()
.map(DeploymentDto::fromDeployment)
.collect(Collectors.toList());
}

// Deployments/activity are keyed only by environment id; scope to the current repository so a
// foreign environment id cannot surface another repository's deployment data (the removed
// gitRepositoryFilter used to enforce this on the derived Deployment queries).
private boolean environmentInCurrentRepository(Long environmentId) {
Long repositoryId = RepositoryContext.getRepositoryId();
return repositoryId != null
&& environmentRepository
.findByIdAndRepositoryRepositoryId(environmentId, repositoryId)
.isPresent();
}

public List<DeploymentDto> getDeploymentsByEnvironmentId(Long environmentId) {
if (!environmentInCurrentRepository(environmentId)) {
return List.of();
}
return deploymentRepository.findByEnvironmentIdOrderByCreatedAtDesc(environmentId).stream()
.map(DeploymentDto::fromDeployment)
.collect(Collectors.toList());
}

public Optional<DeploymentDto> getLatestDeploymentByEnvironmentId(Long environmentId) {
if (!environmentInCurrentRepository(environmentId)) {
return Optional.empty();
}
return deploymentRepository
.findFirstByEnvironmentIdOrderByCreatedAtDesc(environmentId)
.map(DeploymentDto::fromDeployment);
Expand Down Expand Up @@ -298,6 +323,9 @@ private boolean canRedeploy(Environment environment, long timeoutMinutes) {
* </ul>
*/
public List<ActivityHistoryDto> getActivityHistoryByEnvironmentId(Long environmentId) {
if (!environmentInCurrentRepository(environmentId)) {
return List.of();
}
// 1) Real deployments
List<ActivityHistoryDto> deploymentDtos =
deploymentRepository.findByEnvironmentIdOrderByCreatedAtDesc(environmentId).stream()
Expand Down Expand Up @@ -356,7 +384,9 @@ public List<ActivityHistoryDto> getActivityHistoryByEnvironmentId(Long environme
* @throws EntityNotFoundException if the pull request does not exist
*/
public List<ActivityHistoryDto> getActivityHistoryByPullRequestId(Long pullRequestId) {
if (pullRequestRepository.findById(pullRequestId).isEmpty()) {
if (pullRequestRepository
.findByIdAndRepositoryRepositoryId(pullRequestId, RepositoryContext.getRepositoryId())
.isEmpty()) {
throw new EntityNotFoundException("Pull request not found with id: " + pullRequestId);
}

Expand Down Expand Up @@ -420,7 +450,6 @@ public List<ActivityHistoryDto> getActivityHistoryByRepositoryIdAndBranchName(
* @return Basic success message
* @throws DeploymentException if the deployment cannot be canceled
*/
@Transactional
public String cancelDeployment(CancelDeploymentRequest cancelRequest) {
Long workflowRunId = cancelRequest.workflowRunId();

Expand Down Expand Up @@ -523,20 +552,34 @@ public WorkflowJobsResponse getWorkflowJobStatus(Long workflowRunId) {
}

private String resolveWorkflowRunRepositoryName(Long workflowRunId) {
// Resolve strictly within the current repository so a foreign run id can never resolve another
// repository's name (which would proxy that repo's GitHub job status back to the caller).
final Long repositoryId = RepositoryContext.getRepositoryId();
if (repositoryId == null) {
throw new DeploymentException(
"No repository context to resolve workflow run " + workflowRunId);
}
return workflowRunRepository
.findById(workflowRunId.longValue())
.findByIdAndRepositoryRepositoryId(workflowRunId, repositoryId)
.map(workflowRun -> workflowRun.getRepository().getNameWithOwner())
.or(
() ->
heliosDeploymentRepository
.findByWorkflowRunId(workflowRunId)
.filter(
heliosDeployment ->
repositoryId.equals(
heliosDeployment
.getEnvironment()
.getRepository()
.getRepositoryId()))
.map(
heliosDeployment ->
heliosDeployment.getEnvironment().getRepository().getNameWithOwner()))
.or(
() ->
Optional.ofNullable(RepositoryContext.getRepositoryId())
.flatMap(gitRepoRepository::findById)
gitRepoRepository
.findById(repositoryId)
.map(GitRepository::getNameWithOwner))
.orElseThrow(
() ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

@Repository
public interface EnvironmentRepository extends JpaRepository<Environment, Long> {
List<Environment> findAllByOrderByNameAsc();

List<Environment> findByRepository(GitRepository repository);

@Query("SELECT e FROM Environment e "
Expand All @@ -22,7 +20,12 @@ public interface EnvironmentRepository extends JpaRepository<Environment, Long>

List<Environment> findByRepositoryRepositoryIdOrderByCreatedAtDesc(Long repositoryId);

List<Environment> findByEnabledTrueOrderByNameAsc();
// Explicit per-repository scoped finders (Option B: replaces the ambient gitRepositoryFilter).
List<Environment> findByRepositoryRepositoryIdOrderByNameAsc(Long repositoryId);

List<Environment> findByEnabledTrueAndRepositoryRepositoryIdOrderByNameAsc(Long repositoryId);

Optional<Environment> findByIdAndRepositoryRepositoryId(Long id, Long repositoryId);

@Query("SELECT DISTINCT e FROM Environment e "
+ "LEFT JOIN FETCH e.statusHistory es "
Expand Down
Loading
Loading