Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Add logging of failed authorization attempts to the VCS access log #10369

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ public enum AuthenticationMechanism {
* The user attempted to authenticate to the LocalVC using either a user token or a participation token
*/
VCS_ACCESS_TOKEN,
/**
* The user used HTTPS
*/
HTTPS,
/**
* It is unclear what the user used
*/
OTHER
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
Expand All @@ -49,6 +50,7 @@
import de.tum.cit.aet.artemis.core.security.SecurityUtils;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.util.TimeLogUtil;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.participation.Participation;
import de.tum.cit.aet.artemis.programming.domain.AuthenticationMechanism;
import de.tum.cit.aet.artemis.programming.domain.Commit;
Expand Down Expand Up @@ -255,8 +257,15 @@ public void authenticateAndAuthorizeGitRequest(HttpServletRequest request, Repos
throw new LocalVCForbiddenException();
}

var optionalParticipation = authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryAction, localVCRepositoryUri, false);
savePreliminaryVcsAccessLogForHTTPs(request, localVCRepositoryUri, user, repositoryAction, optionalParticipation);
try {
var optionalParticipation = authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryAction, localVCRepositoryUri, false);
savePreliminaryVcsAccessLogForHTTPs(request, localVCRepositoryUri, user, repositoryAction, optionalParticipation);
}
catch (LocalVCForbiddenException e) {
log.error("User {} does not have access to the repository {}", user.getLogin(), localVCRepositoryUri);
saveFailedAccessVcsAccessLog(Optional.of(request), Optional.empty(), repositoryTypeOrUserName, exercise, localVCRepositoryUri, user, repositoryAction);
throw new AccessDeniedException("User does not have access to this repository", e);
}

log.debug("Authorizing user {} for repository {} took {}", user.getLogin(), localVCRepositoryUri, TimeLogUtil.formatDurationFrom(timeNanoStart));
}
Expand All @@ -278,18 +287,83 @@ private void savePreliminaryVcsAccessLogForHTTPs(HttpServletRequest request, Loc
var ipAddress = request.getRemoteAddr();
var authenticationMechanism = resolveHTTPSAuthenticationMechanism(request.getHeader(LocalVCServletService.AUTHORIZATION_HEADER), user);

String commitHash = null;
String finalCommitHash = getCommitHash(localVCRepositoryUri);
RepositoryActionType finalRepositoryAction = repositoryAction == RepositoryActionType.WRITE ? RepositoryActionType.PUSH : RepositoryActionType.PULL;
vcsAccessLogService.ifPresent(service -> service.saveAccessLog(user, participation, finalRepositoryAction, authenticationMechanism, finalCommitHash, ipAddress));
}
}

/**
* Logs a failed attempt to access a repository.
*
* @param request An optional {@link HttpServletRequest} containing request details if access was attempted via HTTPS.
* @param session An optional {@link ServerSession} containing session details if access was attempted via SSH.
* @param repositoryTypeOrUserName A string representing either the repository type or the username associated with the repository.
* @param exercise The {@link Exercise} associated with the repository.
* @param localVCRepositoryUri The {@link LocalVCRepositoryUri} representing the repository location.
* @param user The {@link User} attempting the access.
* @param repositoryAction The {@link RepositoryActionType} action that was attempted.
*/
public void saveFailedAccessVcsAccessLog(Optional<HttpServletRequest> request, Optional<ServerSession> session, String repositoryTypeOrUserName, Exercise exercise,
LocalVCRepositoryUri localVCRepositoryUri, User user, RepositoryActionType repositoryAction) {
var participation = tryToLoadParticipation(false, repositoryTypeOrUserName, localVCRepositoryUri, (ProgrammingExercise) exercise);
var commitHash = getCommitHash(localVCRepositoryUri);
var authenticationMechanism = resolveAuthenticationMechanismFromSessionOrRequest(request, session, user);
var action = repositoryAction == RepositoryActionType.WRITE ? RepositoryActionType.PUSH_FAIL : RepositoryActionType.CLONE_FAIL;
var ipAddress = request.isPresent() ? request.get().getRemoteAddr() : (session.isPresent() ? session.get().getClientAddress().toString() : "");
vcsAccessLogService.ifPresent(service -> service.saveAccessLog(user, participation, action, authenticationMechanism, commitHash, ipAddress));
}

/**
* Determines the authentication mechanism based on the provided session or request.
*
* <p>
* If a {@link ServerSession} is present, the authentication mechanism is assumed to be SSH.
* </p>
* <p>
* If an {@link HttpServletRequest} is present, the method attempts to resolve the authentication
* mechanism using the authorization header. If an exception occurs, HTTPS authentication is assumed by default.
* </p>
* <p>
* If neither a session nor a request is available, the authentication mechanism defaults to OTHER.
* </p>
*
* @param request an {@link Optional} containing the HTTP request, if available
* @param session an {@link Optional} containing the server session, if available
* @param user the user for whom authentication is being determined
* @return the resolved {@link AuthenticationMechanism}
*/
private AuthenticationMechanism resolveAuthenticationMechanismFromSessionOrRequest(Optional<HttpServletRequest> request, Optional<ServerSession> session, User user) {
if (session.isPresent()) {
return AuthenticationMechanism.SSH;
}
else if (request.isPresent()) {
try {
commitHash = getLatestCommitHash(repositories.get(localVCRepositoryUri.getRelativeRepositoryPath().toString()));
return resolveHTTPSAuthenticationMechanism(request.get().getHeader(LocalVCServletService.AUTHORIZATION_HEADER), user);
}
catch (GitAPIException e) {
log.warn("Failed to obtain commit hash for repository {}. Error: {}", localVCRepositoryUri.getRelativeRepositoryPath().toString(), e.getMessage());
catch (LocalVCAuthException ignored) {
return AuthenticationMechanism.HTTPS;
}
}
else {
return AuthenticationMechanism.OTHER;
}
}

String finalCommitHash = commitHash;
RepositoryActionType finalRepositoryAction = repositoryAction == RepositoryActionType.WRITE ? RepositoryActionType.PUSH : RepositoryActionType.PULL;
vcsAccessLogService.ifPresent(service -> service.saveAccessLog(user, participation, finalRepositoryAction, authenticationMechanism, finalCommitHash, ipAddress));
/**
* Retrieves the latest commit hash from the given repository.
*
* @param localVCRepositoryUri The {@link LocalVCRepositoryUri} representing the repository location.
* @return The latest commit hash as a string, or an empty string if retrieval fails.
*/
private String getCommitHash(LocalVCRepositoryUri localVCRepositoryUri) {
try {
return getLatestCommitHash(repositories.get(localVCRepositoryUri.getRelativeRepositoryPath().toString()));
}
catch (GitAPIException e) {
log.warn("Failed to obtain commit hash for repository {}. Error: {}", localVCRepositoryUri.getRelativeRepositoryPath().toString(), e.getMessage());
}
return "";
}

/**
Expand Down Expand Up @@ -482,7 +556,7 @@ private UsernameAndPassword extractUsernameAndPassword(String authorizationHeade
* @param repositoryActionType The type of the action the user wants to perform.
* @param localVCRepositoryUri The URI of the local repository.
* @param usingSSH The flag specifying whether the method is called from the SSH or HTTPs context
* @return the ProgrammingParticipation Optional, containing the participation fetched during authorization
* @return the ProgrammingParticipation Optional, containing the fetched participation
* @throws LocalVCForbiddenException If the user is not allowed to access the repository.
*/
public Optional<ProgrammingExerciseParticipation> authorizeUser(String repositoryTypeOrUserName, User user, ProgrammingExercise exercise,
Expand All @@ -499,6 +573,16 @@ public Optional<ProgrammingExerciseParticipation> authorizeUser(String repositor
return Optional.of(participation);
}

/**
* Attempts to load a programming exercise participation based on the provided parameters.
*
* @param usingSSH {@code true} if the user's session is over SSH, {@code false} if over HTTP
* @param repositoryTypeOrUserName A string representing either the repository type or the username associated with the repository.
* @param localVCRepositoryUri The local version control repository URI.
* @param exercise The programming exercise for which participation is being fetched.
* @return The fetched {@link ProgrammingExerciseParticipation} instance.
* @throws LocalVCInternalException If no participation is found and it is not an auxiliary repository.
*/
private ProgrammingExerciseParticipation tryToLoadParticipation(boolean usingSSH, String repositoryTypeOrUserName, LocalVCRepositoryUri localVCRepositoryUri,
ProgrammingExercise exercise) throws LocalVCInternalException {
ProgrammingExerciseParticipation participation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.Optional;

import org.apache.sshd.git.GitLocationResolver;
import org.apache.sshd.server.session.ServerSession;
Expand Down Expand Up @@ -83,6 +84,8 @@ public Path resolveRootDirectory(String command, String[] args, ServerSession se
}
catch (LocalVCForbiddenException e) {
log.error("User {} does not have access to the repository {}", user.getLogin(), repositoryPath);
localVCServletService.saveFailedAccessVcsAccessLog(Optional.empty(), Optional.of(session), repositoryTypeOrUserName, exercise, localVCRepositoryUri, user,
repositoryAction);
throw new AccessDeniedException("User does not have access to this repository", e);
}
}
Expand Down
Loading