diff --git a/alpine-common/src/main/java/alpine/Config.java b/alpine-common/src/main/java/alpine/Config.java index f720d324..2b12cb3f 100644 --- a/alpine-common/src/main/java/alpine/Config.java +++ b/alpine-common/src/main/java/alpine/Config.java @@ -30,7 +30,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.UUID; @@ -161,6 +163,7 @@ public enum AlpineKey implements Key { OIDC_USER_PROVISIONING ("alpine.oidc.user.provisioning", false), OIDC_TEAM_SYNCHRONIZATION ("alpine.oidc.team.synchronization", false), OIDC_TEAMS_CLAIM ("alpine.oidc.teams.claim", "groups"), + OIDC_TEAMS_DEFAULT ("alpine.oidc.teams.default", null), HTTP_PROXY_ADDRESS ("alpine.http.proxy.address", null), HTTP_PROXY_PORT ("alpine.http.proxy.port", null), HTTP_PROXY_USERNAME ("alpine.http.proxy.username", null), @@ -482,6 +485,22 @@ public boolean getPropertyAsBoolean(Key key) { return "true".equalsIgnoreCase(getProperty(key)); } + /** + * Return the configured value for the specified Key. + * @param key The Key to return the configuration for + * @return a list of the comma-separated values of the configuration, + * or an empty list otherwise + * @since 2.2.5 + */ + public List getPropertyAsList(Key key) { + String property = getProperty(key); + if (property == null) { + return Collections.emptyList(); + } else { + return List.of(property.split(",")); + } + } + /** * Get "pass-through" properties with a given {@code prefix}. *

diff --git a/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index 300faea9..da128a32 100644 --- a/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -312,6 +312,30 @@ public OidcUser synchronizeTeamMembership(final OidcUser user, final List teamNames) { + LOGGER.debug("Synchronizing team membership for OpenID Connect user " + user.getUsername()); + + for (final String teamName : teamNames) { + Team team = getTeam(teamName); + if (team == null) { + LOGGER.warn("Cannot add user " + user.getUsername() + " to team " + teamName + ", because no team with that name exists"); + } else { + LOGGER.debug("Adding user: " + user.getUsername() + " to team: " + teamName); + addUserToTeam(user, team); + } + } + + return getObjectById(OidcUser.class, user.getId()); + } + /** * Retrieves an LdapUser containing the specified username. If the username * does not exist, returns null. @@ -558,6 +582,20 @@ public List getTeams() { return (List) query.execute(); } + /** + * Returns a Team containing the specified name. If the name + * does not exist, returns null. + * @param name Name of the team to retrieve + * @return a Team + * @since 2.2.5 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public Team getTeam(final String name) { + final Query query = pm.newQuery(Team.class, "name == :name"); + final List result = (List) query.execute(name); + return Collections.isEmpty(result) ? null : result.get(0); + } + /** * Updates the specified Team. * @param transientTeam the optionally detached Team object to update diff --git a/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java b/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java index c5c5588e..3ffa0fbd 100644 --- a/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java +++ b/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java @@ -235,9 +235,10 @@ private OidcUser autoProvision(final AlpineQueryManager qm, final OidcProfile pr if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { LOGGER.debug("Synchronizing teams for user " + user.getUsername()); return qm.synchronizeTeamMembership(user, profile.getGroups()); + } else { + // Only apply default teams during auto-provisioning, not on later updates: + return qm.addUserToTeams(user, config.getPropertyAsList(Config.AlpineKey.OIDC_TEAMS_DEFAULT)); } - - return user; } } diff --git a/alpine-server/src/test/java/alpine/server/auth/OidcAuthenticationServiceTest.java b/alpine-server/src/test/java/alpine/server/auth/OidcAuthenticationServiceTest.java index 1abaabda..309d3b2d 100644 --- a/alpine-server/src/test/java/alpine/server/auth/OidcAuthenticationServiceTest.java +++ b/alpine-server/src/test/java/alpine/server/auth/OidcAuthenticationServiceTest.java @@ -320,6 +320,35 @@ public void authenticateShouldProvisionAndReturnNewUserWhenUserDoesNotExistAndPr Assertions.assertThat(provisionedUser.getPermissions()).isNullOrEmpty(); } + @Test + public void authenticateShouldProvisionAndApplyDefaultTeamsAndReturnNewUserWhenUserDoesNotExistAndProvisioningIsEnabled() throws Exception { + Mockito.when(configMock.getPropertyAsBoolean(ArgumentMatchers.eq(Config.AlpineKey.OIDC_USER_PROVISIONING))).thenReturn(true); + Mockito.when(configMock.getPropertyAsList(ArgumentMatchers.eq(Config.AlpineKey.OIDC_TEAMS_DEFAULT))).thenReturn(List.of("teamName")); + + try (final var qm = new AlpineQueryManager()) { + var teamToAssign = new Team(); + teamToAssign.setName("teamName"); + qm.persist(teamToAssign); + } + + final var profile = new OidcProfile(); + profile.setSubject("subject"); + profile.setUsername("username"); + profile.setEmail("username@example.com"); + Mockito.when(idTokenAuthenticatorMock.authenticate(ArgumentMatchers.eq(ID_TOKEN), ArgumentMatchers.any(OidcProfileCreator.class))).thenReturn(profile); + + final var authService = new OidcAuthenticationService(configMock, oidcConfigurationMock, idTokenAuthenticatorMock, null, ID_TOKEN, null); + + final var provisionedUser = (OidcUser) authService.authenticate(); + Assertions.assertThat(provisionedUser).isNotNull(); + Assertions.assertThat(provisionedUser.getUsername()).isEqualTo("username"); + Assertions.assertThat(provisionedUser.getSubjectIdentifier()).isEqualTo("subject"); + Assertions.assertThat(provisionedUser.getEmail()).isEqualTo("username@example.com"); + Assertions.assertThat(provisionedUser.getTeams()).hasSize(1); + Assertions.assertThat(provisionedUser.getTeams().get(0).getName()).isEqualTo("teamName"); + Assertions.assertThat(provisionedUser.getPermissions()).isNullOrEmpty(); + } + @Test public void authenticateShouldProvisionAndSyncTeamsAndReturnNewUserWhenUserDoesNotExistAndProvisioningAndTeamSyncIsEnabled() throws Exception { Mockito.when(configMock.getPropertyAsBoolean(ArgumentMatchers.eq(Config.AlpineKey.OIDC_USER_PROVISIONING))).thenReturn(true);