diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java index 36839d324440..71e4dc0a5775 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java @@ -332,6 +332,11 @@ public final class Constants { */ public static final String PROFILE_LTI = "lti"; + /** + * The name of the Spring profile used for activating SAML2 in Artemis, see {@link de.tum.cit.aet.artemis.core.service.connectors.SAML2Service}. + */ + public static final String PROFILE_SAML2 = "saml2"; + public static final String PROFILE_SCHEDULING = "scheduling"; /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Configuration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Configuration.java index 46fcbd938246..f8e25073785a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Configuration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Configuration.java @@ -38,7 +38,7 @@ * Describes the security configuration for SAML2. */ @Configuration -@Profile("saml2") +@Profile(Constants.PROFILE_SAML2) public class SAML2Configuration { private static final Logger log = LoggerFactory.getLogger(SAML2Configuration.class); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Properties.java b/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Properties.java index b3d70017ae6d..bb13639c3592 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Properties.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/SAML2Properties.java @@ -18,7 +18,7 @@ */ @Profile(PROFILE_CORE) @Component -@ConfigurationProperties("saml2") +@ConfigurationProperties(Constants.PROFILE_SAML2) public class SAML2Properties { /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CustomAuditEventRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CustomAuditEventRepository.java index 59f0789404f4..c23bf1ebd2fe 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CustomAuditEventRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CustomAuditEventRepository.java @@ -6,14 +6,18 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Repository; +import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.config.audit.AuditEventConverter; import de.tum.cit.aet.artemis.core.domain.PersistentAuditEvent; @@ -24,6 +28,10 @@ @Repository public class CustomAuditEventRepository implements AuditEventRepository { + private final boolean isSaml2Active; + + private static final String AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS"; + private static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE"; /** @@ -37,9 +45,10 @@ public class CustomAuditEventRepository implements AuditEventRepository { private static final Logger log = LoggerFactory.getLogger(CustomAuditEventRepository.class); - public CustomAuditEventRepository(PersistenceAuditEventRepository persistenceAuditEventRepository, AuditEventConverter auditEventConverter) { + public CustomAuditEventRepository(Environment environment, PersistenceAuditEventRepository persistenceAuditEventRepository, AuditEventConverter auditEventConverter) { this.persistenceAuditEventRepository = persistenceAuditEventRepository; this.auditEventConverter = auditEventConverter; + this.isSaml2Active = Set.of(environment.getActiveProfiles()).contains(Constants.PROFILE_SAML2); } @Override @@ -51,6 +60,11 @@ public List find(String principal, Instant after, String type) { @Override public void add(AuditEvent event) { if (!AUTHORIZATION_FAILURE.equals(event.getType())) { + if (isSaml2Active && AUTHENTICATION_SUCCESS.equals(event.getType()) && SecurityContextHolder.getContext().getAuthentication() == null) { + // If authentication is null, Auth is success, and SAML2 profile is active => SAML2 authentication is running. + // Logging is handled manually. + return; + } PersistentAuditEvent persistentAuditEvent = new PersistentAuditEvent(); persistentAuditEvent.setPrincipal(event.getPrincipal()); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/connectors/SAML2Service.java b/src/main/java/de/tum/cit/aet/artemis/core/service/connectors/SAML2Service.java index 8ecc8a081106..93a0d5bef582 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/connectors/SAML2Service.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/connectors/SAML2Service.java @@ -1,6 +1,11 @@ package de.tum.cit.aet.artemis.core.service.connectors; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SAML2; +import static de.tum.cit.aet.artemis.core.config.Constants.SYSTEM_ACCOUNT; + +import java.time.Instant; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; @@ -12,6 +17,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.context.annotation.Profile; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -35,7 +42,7 @@ /** * This class describes a service for SAML2 authentication. *

- * The main method is {@link #handleAuthentication(Saml2AuthenticatedPrincipal)}. The service extracts the user information + * The main method is {@link #handleAuthentication(Authentication,Saml2AuthenticatedPrincipal)}. The service extracts the user information * from the {@link Saml2AuthenticatedPrincipal} and creates the user, if it does not exist already. *

* When the user gets created, the SAML2 attributes can be used to fill in user information. The configuration happens @@ -45,9 +52,11 @@ * This is needed, since the client "does not know" that he is already authenticated via SAML2. */ @Service -@Profile("saml2") +@Profile(PROFILE_SAML2) public class SAML2Service { + private final AuditEventRepository auditEventRepository; + @Value("${info.saml2.enable-password:#{null}}") private Optional saml2EnablePassword; @@ -68,12 +77,14 @@ public class SAML2Service { /** * Constructs a new instance. * - * @param userRepository The user repository - * @param properties The properties - * @param userCreationService The user creation service + * @param auditEventRepository The audit event repository + * @param userRepository The user repository + * @param properties The properties + * @param userCreationService The user creation service */ - public SAML2Service(final UserRepository userRepository, final SAML2Properties properties, final UserCreationService userCreationService, MailService mailService, - UserService userService) { + public SAML2Service(final AuditEventRepository auditEventRepository, final UserRepository userRepository, final SAML2Properties properties, + final UserCreationService userCreationService, MailService mailService, UserService userService) { + this.auditEventRepository = auditEventRepository; this.userRepository = userRepository; this.properties = properties; this.userCreationService = userCreationService; @@ -93,10 +104,13 @@ private Map generateExtractionPatterns(final SAML2Properties pr *

* Registers new users and returns a new {@link UsernamePasswordAuthenticationToken} matching the SAML2 user. * - * @param principal the principal, containing the user information + * @param originalAuth the original authentication with details + * @param principal the principal, containing the user information * @return a new {@link UsernamePasswordAuthenticationToken} matching the SAML2 user */ - public Authentication handleAuthentication(final Saml2AuthenticatedPrincipal principal) { + public Authentication handleAuthentication(final Authentication originalAuth, final Saml2AuthenticatedPrincipal principal) { + Map details = originalAuth.getDetails() == null ? Map.of() : Map.of("details", originalAuth.getDetails()); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); log.debug("SAML2 User '{}' logged in, attributes {}", auth.getName(), principal.getAttributes()); @@ -107,6 +121,9 @@ public Authentication handleAuthentication(final Saml2AuthenticatedPrincipal pri if (user.isEmpty()) { // create User if not exists user = Optional.of(createUser(username, principal)); + Map accountCreationDetails = new HashMap<>(details); + accountCreationDetails.put("user", user.get().getLogin()); + auditEventRepository.add(new AuditEvent(Instant.now(), SYSTEM_ACCOUNT, "SAML2_ACCOUNT_CREATE", accountCreationDetails)); if (saml2EnablePassword.isPresent() && Boolean.TRUE.equals(saml2EnablePassword.get())) { log.debug("Sending SAML2 creation mail"); @@ -125,6 +142,7 @@ public Authentication handleAuthentication(final Saml2AuthenticatedPrincipal pri } auth = new UsernamePasswordAuthenticationToken(user.get().getLogin(), user.get().getPassword(), toGrantedAuthorities(user.get().getAuthorities())); + auditEventRepository.add(new AuditEvent(Instant.now(), user.get().getLogin(), "SAML2_AUTHENTICATION_SUCCESS", details)); return auth; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index bef3b235459f..44e44a0ff87a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -116,7 +116,7 @@ public ResponseEntity authorizeSAML2(@RequestBody final String body, HttpS log.debug("SAML2 authentication: {}", authentication); try { - authentication = saml2Service.get().handleAuthentication(principal); + authentication = saml2Service.get().handleAuthentication(authentication, principal); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (UserNotActivatedException e) { diff --git a/src/test/java/de/tum/cit/aet/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java b/src/test/java/de/tum/cit/aet/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java index a59a8a81815e..67581e24e939 100644 --- a/src/test/java/de/tum/cit/aet/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_ARTEMIS; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LTI; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SAML2; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; @@ -48,7 +49,7 @@ @ResourceLock("AbstractSpringIntegrationGitlabCIGitlabSamlTest") // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! -@ActiveProfiles({ SPRING_PROFILE_TEST, PROFILE_ARTEMIS, PROFILE_CORE, "gitlabci", "gitlab", "saml2", PROFILE_SCHEDULING, PROFILE_LTI }) +@ActiveProfiles({ SPRING_PROFILE_TEST, PROFILE_ARTEMIS, PROFILE_CORE, "gitlabci", "gitlab", PROFILE_SAML2, PROFILE_SCHEDULING, PROFILE_LTI }) @TestPropertySource(properties = { "artemis.user-management.use-external=false" }) public abstract class AbstractSpringIntegrationGitlabCIGitlabSamlTest extends AbstractArtemisIntegrationTest {