Skip to content

Commit

Permalink
Development: Add security check and profile restrictions for build qu…
Browse files Browse the repository at this point in the history
…eue representation in local continuous integration (#7843)
  • Loading branch information
mateusmm01 authored Jan 7, 2024
1 parent 798620a commit 00d9172
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static de.tum.in.www1.artemis.web.websocket.ResultWebsocketService.getExerciseIdFromNonPersonalExerciseResultDestination;
import static de.tum.in.www1.artemis.web.websocket.ResultWebsocketService.isNonPersonalExerciseResultDestination;
import static de.tum.in.www1.artemis.web.websocket.localci.LocalCIBuildQueueWebsocketService.isBuildQueueAdminDestination;
import static de.tum.in.www1.artemis.web.websocket.localci.LocalCIBuildQueueWebsocketService.isBuildQueueCourseDestination;
import static de.tum.in.www1.artemis.web.websocket.team.ParticipationTeamWebsocketService.*;

import java.net.InetSocketAddress;
Expand Down Expand Up @@ -46,13 +48,11 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Iterators;

import de.tum.in.www1.artemis.domain.Course;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.repository.ExamRepository;
import de.tum.in.www1.artemis.repository.ExerciseRepository;
import de.tum.in.www1.artemis.repository.StudentParticipationRepository;
import de.tum.in.www1.artemis.repository.UserRepository;
import de.tum.in.www1.artemis.repository.*;
import de.tum.in.www1.artemis.security.Role;
import de.tum.in.www1.artemis.security.jwt.JWTFilter;
import de.tum.in.www1.artemis.security.jwt.TokenProvider;
Expand Down Expand Up @@ -96,9 +96,11 @@ public class WebsocketConfiguration extends DelegatingWebSocketMessageBrokerConf
@Value("${spring.websocket.broker.password}")
private String brokerPassword;

private final CourseRepository courseRepository;

public WebsocketConfiguration(MappingJackson2HttpMessageConverter springMvcJacksonConverter, TaskScheduler messageBrokerTaskScheduler, TokenProvider tokenProvider,
StudentParticipationRepository studentParticipationRepository, AuthorizationCheckService authorizationCheckService, ExerciseRepository exerciseRepository,
UserRepository userRepository, ExamRepository examRepository) {
UserRepository userRepository, ExamRepository examRepository, CourseRepository courseRepository) {
this.objectMapper = springMvcJacksonConverter.getObjectMapper();
this.messageBrokerTaskScheduler = messageBrokerTaskScheduler;
this.tokenProvider = tokenProvider;
Expand All @@ -107,6 +109,7 @@ public WebsocketConfiguration(MappingJackson2HttpMessageConverter springMvcJacks
this.exerciseRepository = exerciseRepository;
this.userRepository = userRepository;
this.examRepository = examRepository;
this.courseRepository = courseRepository;
}

@Override
Expand Down Expand Up @@ -262,12 +265,34 @@ public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel c

/**
* Returns whether the subscription of the given principal to the given destination is permitted
* Database calls should be avoided as much as possible in this method.
* Only for very specific topics, database calls are allowed.
*
* @param principal User principal of the user who wants to subscribe
* @param destination Destination topic to which the user wants to subscribe
* @return flag whether subscription is allowed
*/
private boolean allowSubscription(Principal principal, String destination) {
/*
* IMPORTANT: Avoid database calls in this method as much as possible (e.g. checking if the user
* is an instructor in a course)
* This method is called for every subscription request, so it should be as fast as possible.
* If you need to do a database call, make sure to first check if the destination is valid for your specific
* use case.
*/

if (isBuildQueueAdminDestination(destination)) {
var user = userRepository.getUserWithAuthorities(principal.getName());
return authorizationCheckService.isAdmin(user);
}

Optional<Long> courseId = isBuildQueueCourseDestination(destination);
if (courseId.isPresent()) {
Course course = courseRepository.findByIdElseThrow(courseId.get());
var user = userRepository.getUserWithGroupsAndAuthorities(principal.getName());
return authorizationCheckService.isAtLeastInstructorInCourse(course, user);
}

if (isParticipationTeamDestination(destination)) {
Long participationId = getParticipationIdFromDestination(destination);
return isParticipationOwnedByUser(principal, participationId);
Expand All @@ -288,7 +313,7 @@ private boolean allowSubscription(Principal principal, String destination) {
var examId = getExamIdFromExamRootDestination(destination);
if (examId.isPresent()) {
var exam = examRepository.findByIdElseThrow(examId.get());
User user = userRepository.getUserWithGroupsAndAuthorities(principal.getName());
var user = userRepository.getUserWithGroupsAndAuthorities(principal.getName());
return authorizationCheckService.isAtLeastInstructorInCourse(exam.getCourse(), user);
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificat
@EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" })
Optional<User> findOneWithGroupsAndAuthoritiesByLogin(String login);

@EntityGraph(type = LOAD, attributePaths = { "authorities" })
Optional<User> findOneWithAuthoritiesByLogin(String login);

@EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" })
Optional<User> findOneWithGroupsAndAuthoritiesByEmail(String email);

Expand Down Expand Up @@ -499,6 +502,17 @@ default User getUserWithGroupsAndAuthorities() {
return unwrapOptionalUser(user, currentUserLogin);
}

/**
* Get user with authorities of currently logged-in user
*
* @return currently logged-in user
*/
default User getUserWithAuthorities() {
String currentUserLogin = getCurrentUserLogin();
Optional<User> user = findOneWithAuthoritiesByLogin(currentUserLogin);
return unwrapOptionalUser(user, currentUserLogin);
}

/**
* Get user with user groups, authorities and organizations of currently logged-in user
*
Expand Down Expand Up @@ -549,6 +563,17 @@ default User getUserWithGroupsAndAuthorities(@NotNull String username) {
return unwrapOptionalUser(user, username);
}

/**
* Get user with authorities with the username (i.e. user.getLogin() or principal.getName())
*
* @param username the username of the user who should be retrieved from the database
* @return the user that belongs to the given principal with eagerly loaded authorities
*/
default User getUserWithAuthorities(@NotNull String username) {
Optional<User> user = findOneWithAuthoritiesByLogin(username);
return unwrapOptionalUser(user, username);
}

/**
* Finds a single user with groups and authorities using the registration number
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package de.tum.in.www1.artemis.service.connectors.localci;

import java.util.Objects;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import com.hazelcast.collection.IQueue;
import com.hazelcast.collection.ItemEvent;
import com.hazelcast.collection.ItemListener;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import com.hazelcast.map.listener.EntryAddedListener;
import com.hazelcast.map.listener.EntryRemovedListener;

import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildAgentInformation;
import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildJobQueueItem;
import de.tum.in.www1.artemis.web.websocket.localci.LocalCIBuildQueueWebsocketService;

/**
* This service is responsible for sending build job queue information over websockets.
* It listens to changes in the build job queue and sends the updated information to the client.
* NOTE: This service is only active if the profile "localci" and "scheduling" are active. This avoids sending the
* same information multiple times and thus also avoids unnecessary load on the server.
*/
@Service
@Profile("localci & scheduling")
public class LocalCIQueueWebsocketService {

private final Logger log = LoggerFactory.getLogger(LocalCIQueueWebsocketService.class);

private final HazelcastInstance hazelcastInstance;

private final IQueue<LocalCIBuildJobQueueItem> queue;

private final IMap<Long, LocalCIBuildJobQueueItem> processingJobs;

private final IMap<String, LocalCIBuildAgentInformation> buildAgentInformation;

private final LocalCIBuildQueueWebsocketService localCIBuildQueueWebsocketService;

private final LocalCISharedBuildJobQueueService localCISharedBuildJobQueueService;

/**
* Instantiates a new Local ci queue websocket service.
*
* @param hazelcastInstance the hazelcast instance
* @param localCIBuildQueueWebsocketService the local ci build queue websocket service
* @param localCISharedBuildJobQueueService the local ci shared build job queue service
*/
public LocalCIQueueWebsocketService(HazelcastInstance hazelcastInstance, LocalCIBuildQueueWebsocketService localCIBuildQueueWebsocketService,
LocalCISharedBuildJobQueueService localCISharedBuildJobQueueService) {
this.hazelcastInstance = hazelcastInstance;
this.localCIBuildQueueWebsocketService = localCIBuildQueueWebsocketService;
this.localCISharedBuildJobQueueService = localCISharedBuildJobQueueService;
this.queue = this.hazelcastInstance.getQueue("buildJobQueue");
this.processingJobs = this.hazelcastInstance.getMap("processingJobs");
this.buildAgentInformation = this.hazelcastInstance.getMap("buildAgentInformation");
}

/**
* Add listeners for build job queue changes.
*/
@PostConstruct
public void addListeners() {
this.queue.addItemListener(new QueuedBuildJobItemListener(), true);
this.processingJobs.addLocalEntryListener(new ProcessingBuildJobItemListener());
this.buildAgentInformation.addLocalEntryListener(new BuildAgentListener());
// localCIBuildQueueWebsocketService will be autowired only if scheduling is active
Objects.requireNonNull(localCIBuildQueueWebsocketService, "localCIBuildQueueWebsocketService must be non-null when scheduling is active.");
}

private void sendQueuedJobsOverWebsocket(long courseId) {
localCIBuildQueueWebsocketService.sendQueuedBuildJobs(localCISharedBuildJobQueueService.getQueuedJobs());
localCIBuildQueueWebsocketService.sendQueuedBuildJobsForCourse(courseId, localCISharedBuildJobQueueService.getQueuedJobsForCourse(courseId));
}

private void sendProcessingJobsOverWebsocket(long courseId) {
localCIBuildQueueWebsocketService.sendRunningBuildJobs(localCISharedBuildJobQueueService.getProcessingJobs());
localCIBuildQueueWebsocketService.sendRunningBuildJobsForCourse(courseId, localCISharedBuildJobQueueService.getProcessingJobsForCourse(courseId));
}

private void sendBuildAgentInformationOverWebsocket() {
localCIBuildQueueWebsocketService.sendBuildAgentInformation(localCISharedBuildJobQueueService.getBuildAgentInformation());
}

private class QueuedBuildJobItemListener implements ItemListener<LocalCIBuildJobQueueItem> {

@Override
public void itemAdded(ItemEvent<LocalCIBuildJobQueueItem> event) {
sendQueuedJobsOverWebsocket(event.getItem().getCourseId());
}

@Override
public void itemRemoved(ItemEvent<LocalCIBuildJobQueueItem> event) {
sendQueuedJobsOverWebsocket(event.getItem().getCourseId());
}
}

private class ProcessingBuildJobItemListener implements EntryAddedListener<Long, LocalCIBuildJobQueueItem>, EntryRemovedListener<Long, LocalCIBuildJobQueueItem> {

@Override
public void entryAdded(com.hazelcast.core.EntryEvent<Long, LocalCIBuildJobQueueItem> event) {
log.debug("CIBuildJobQueueItem added to processing jobs: {}", event.getValue());
sendProcessingJobsOverWebsocket(event.getValue().getCourseId());
}

@Override
public void entryRemoved(com.hazelcast.core.EntryEvent<Long, LocalCIBuildJobQueueItem> event) {
log.debug("CIBuildJobQueueItem removed from processing jobs: {}", event.getOldValue());
sendProcessingJobsOverWebsocket(event.getOldValue().getCourseId());
}
}

private class BuildAgentListener implements EntryAddedListener<String, LocalCIBuildAgentInformation>, EntryRemovedListener<String, LocalCIBuildAgentInformation> {

@Override
public void entryAdded(com.hazelcast.core.EntryEvent<String, LocalCIBuildAgentInformation> event) {
log.debug("Build agent added: {}", event.getValue());
sendBuildAgentInformationOverWebsocket();
}

@Override
public void entryRemoved(com.hazelcast.core.EntryEvent<String, LocalCIBuildAgentInformation> event) {
log.debug("Build agent removed: {}", event.getOldValue());
sendBuildAgentInformationOverWebsocket();
}
}
}
Loading

0 comments on commit 00d9172

Please sign in to comment.