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,8 @@ public enum AuthenticationMechanism {
* The user attempted to authenticate to the LocalVC using either a user token or a participation token
*/
VCS_ACCESS_TOKEN,
/**
* The authentication header was missing
*/
NONE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.tum.cit.aet.artemis.programming.service.localvc;

import jakarta.servlet.http.HttpServletRequest;

import org.apache.sshd.server.session.ServerSession;

public sealed interface AuthenticationContext {

record Session(ServerSession session) implements AuthenticationContext {

@Override
public String getIpAddress() {
return session.getClientAddress().toString();
}
}

record Request(HttpServletRequest request) implements AuthenticationContext {

@Override
public String getIpAddress() {
return request.getRemoteAddr();
}
}

String getIpAddress();
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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 +256,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(new AuthenticationContext.Request(request), repositoryTypeOrUserName, exercise, localVCRepositoryUri, user, repositoryAction);
throw e;
}

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

String commitHash = null;
try {
commitHash = getLatestCommitHash(repositories.get(localVCRepositoryUri.getRelativeRepositoryPath().toString()));
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 context the Authentication context
* @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(AuthenticationContext context, 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(context, user);
var action = repositoryAction == RepositoryActionType.WRITE ? RepositoryActionType.PUSH_FAIL : RepositoryActionType.CLONE_FAIL;
var ipAddress = context.getIpAddress();
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 context the Authentication context
* @param user the user for whom authentication is being determined
* @return the resolved {@link AuthenticationMechanism}
*/
private AuthenticationMechanism resolveAuthenticationMechanismFromSessionOrRequest(AuthenticationContext context, User user) {
switch (context) {
case AuthenticationContext.Session ignored -> {
return AuthenticationMechanism.SSH;
}
catch (GitAPIException e) {
log.warn("Failed to obtain commit hash for repository {}. Error: {}", localVCRepositoryUri.getRelativeRepositoryPath().toString(), e.getMessage());
case AuthenticationContext.Request request -> {
try {
return resolveHTTPSAuthenticationMechanism(request.request().getHeader(LocalVCServletService.AUTHORIZATION_HEADER), user);
}
catch (LocalVCAuthException ignored) {
return AuthenticationMechanism.NONE;
}
}
}
}

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 +552,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 +569,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 @@ -83,6 +83,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(new AuthenticationContext.Session(session), repositoryTypeOrUserName, exercise, localVCRepositoryUri, user,
repositoryAction);
throw new AccessDeniedException("User does not have access to this repository", e);
}
}
Expand Down
Loading