From 66f2942eea748f83a1a10048244a7905df5e0cf6 Mon Sep 17 00:00:00 2001 From: Francisco Javier Fernandez Gonzalez Date: Fri, 30 Aug 2024 12:22:55 +0200 Subject: [PATCH] Add test for oic-auth plugin --- pom.xml | 32 +++ .../acceptance/po/OicAuthSecurityRealm.java | 35 +++ .../utils/keycloack/KeycloakUtils.java | 95 ++++++++ src/test/java/plugins/OicAuthPluginTest.java | 227 ++++++++++++++++++ 4 files changed, 389 insertions(+) create mode 100644 src/main/java/org/jenkinsci/test/acceptance/po/OicAuthSecurityRealm.java create mode 100644 src/main/java/org/jenkinsci/test/acceptance/utils/keycloack/KeycloakUtils.java create mode 100644 src/test/java/plugins/OicAuthPluginTest.java diff --git a/pom.xml b/pom.xml index fb90bf63aa..d950387ea8 100644 --- a/pom.xml +++ b/pom.xml @@ -314,6 +314,28 @@ 2.1.3 test + + + com.github.dasniko + testcontainers-keycloak + 3.4.0 + test + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + test + + + jakarta.xml.bind + jakarta.xml.bind-api + 3.0.1 + @@ -386,6 +408,16 @@ and httpcore 4.4.16 + + + jakarta.ws.rs + jakarta.ws.rs-api + 3.1.0 + diff --git a/src/main/java/org/jenkinsci/test/acceptance/po/OicAuthSecurityRealm.java b/src/main/java/org/jenkinsci/test/acceptance/po/OicAuthSecurityRealm.java new file mode 100644 index 0000000000..d8aa7e3772 --- /dev/null +++ b/src/main/java/org/jenkinsci/test/acceptance/po/OicAuthSecurityRealm.java @@ -0,0 +1,35 @@ +package org.jenkinsci.test.acceptance.po; + +/** + * Security Realm provided by oic-auth plugin + */ +@Describable({"Login with Openid Connect", "Login with Openid Connect"}) +public class OicAuthSecurityRealm extends SecurityRealm { + + public OicAuthSecurityRealm(GlobalSecurityConfig context, String path) { + super(context, path); + } + + public void configureClient(String clientId, String clientSecret) { + control("clientId").set(clientId); + control("clientSecret").set(clientSecret); + } + + public void setAutomaticConfiguration(String wellKnownEndpoint) { + control(by.radioButton("Automatic configuration")).click(); + control("wellKnownOpenIDConfigurationUrl").set(wellKnownEndpoint); + } + + public void logoutFromOpenidProvider(boolean logout) { + Control check = control(by.checkbox("Logout from OpenID Provider")); + if (logout) { + check.check(); + } else { + check.uncheck(); + } + } + + public void setPostLogoutUrl(String postLogoutUrl) { + control("postLogoutRedirectUrl").set(postLogoutUrl); + } +} diff --git a/src/main/java/org/jenkinsci/test/acceptance/utils/keycloack/KeycloakUtils.java b/src/main/java/org/jenkinsci/test/acceptance/utils/keycloack/KeycloakUtils.java new file mode 100644 index 0000000000..1b4f9f6e0a --- /dev/null +++ b/src/main/java/org/jenkinsci/test/acceptance/utils/keycloack/KeycloakUtils.java @@ -0,0 +1,95 @@ +package org.jenkinsci.test.acceptance.utils.keycloack; + +import java.net.URL; + +import org.jenkinsci.test.acceptance.po.CapybaraPortingLayerImpl; +import org.jenkinsci.test.acceptance.utils.ElasticTime; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import jakarta.inject.Inject; + +public class KeycloakUtils extends CapybaraPortingLayerImpl { + + @Inject + public WebDriver driver; + @Inject + public ElasticTime time; + + public KeycloakUtils() { + super(null); + } + + public void open(URL url) { + visit(url); + } + + public void login(String user) { + login(user, user); + } + + public void login(String user, String passwd) { + waitFor(by.id("username"), 5); + find(by.id("username")).sendKeys(user); + find(by.id("password")).sendKeys(passwd); + find(by.id("kc-login")).click(); + } + + + public User getUser(String keycloakUrl, String realm) { + driver.get(String.format("%s/realms/%s/account", keycloakUrl, realm)); + + waitFor(by.id("username"), 5); + String username = find(by.id("username")).getDomProperty("value"); + String email = find(by.id("email")).getDomProperty("value"); + String firstName = find(by.id("firstName")).getDomProperty("value"); + String lastName = find(by.id("lastName")).getDomProperty("value"); + + + return new User(null /* id not available in this page*/, username, email, firstName, lastName); + } + + public void logout(User user) { + final String caption = user.getFirstName() + " " + user.getLastName(); + waitFor(by.button(caption), 5); + clickButton(caption); + waitFor(by.button("Sign out")); + clickButton("Sign out"); + } + + public static class User { + + private final String id; + private final String userName; + private final String email; + private final String firstName; + private final String lastName; + + public User(String id, String userName, String email, String firstName, String lastName) { + this.id = id; + this.userName = userName; + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getId() { + return id; + } + + public String getUserName() { + return userName; + } + + public String getEmail() { + return email; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + } +} diff --git a/src/test/java/plugins/OicAuthPluginTest.java b/src/test/java/plugins/OicAuthPluginTest.java new file mode 100644 index 0000000000..af910dc8bd --- /dev/null +++ b/src/test/java/plugins/OicAuthPluginTest.java @@ -0,0 +1,227 @@ +package plugins; + +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jenkinsci.test.acceptance.junit.AbstractJUnitTest; +import org.jenkinsci.test.acceptance.junit.WithPlugins; +import org.jenkinsci.test.acceptance.po.GlobalSecurityConfig; +import org.jenkinsci.test.acceptance.po.LoggedInAuthorizationStrategy; +import org.jenkinsci.test.acceptance.po.OicAuthSecurityRealm; +import org.jenkinsci.test.acceptance.utils.keycloack.KeycloakUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.GroupResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.openqa.selenium.NoSuchElementException; +import dasniko.testcontainers.keycloak.KeycloakContainer; +import jakarta.inject.Inject; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +@WithPlugins("oic-auth") +public class OicAuthPluginTest extends AbstractJUnitTest { + + private static final String REALM = "test-realm"; + private static final String CLIENT = "jenkins"; + + @Rule + public KeycloakContainer keycloak = new KeycloakContainer(); + + @Inject + public KeycloakUtils keycloakUtils; + + private String userBobKeycloakId; + private String userJohnKeycloakId; + + @Before + public void setUpKeycloak() { + configureOIDCProvider(); + configureRealm(); + } + + private void configureOIDCProvider() { + try (Keycloak keycloakAdmin = keycloak.getKeycloakAdminClient()) { + RealmRepresentation testRealm = new RealmRepresentation(); + testRealm.setRealm(REALM); + testRealm.setId(REALM); + testRealm.setDisplayName(REALM); + testRealm.setEnabled(true); + + keycloakAdmin.realms().create(testRealm); + + // Add groups and subgroups + GroupRepresentation employees = new GroupRepresentation(); + employees.setName("employees"); + + final RealmResource theRealm = keycloakAdmin.realm(REALM); + theRealm.groups().add(employees); + + String groupId = theRealm.groups().groups().get(0).getId(); + + GroupRepresentation devs = new GroupRepresentation(); + devs.setName("devs"); + GroupResource group = theRealm.groups().group(groupId); + group.subGroup(devs); + + GroupRepresentation sales = new GroupRepresentation(); + sales.setName("sales"); + group = theRealm.groups().group(groupId); + group.subGroup(sales); + + // Users + UserRepresentation bob = new UserRepresentation(); + bob.setEmail("bob@acme.org"); + bob.setUsername("bob"); + bob.setFirstName("Bob"); + bob.setLastName("Smith"); + CredentialRepresentation credentials = new CredentialRepresentation(); + credentials.setValue("bob"); + credentials.setTemporary(false); + credentials.setType(CredentialRepresentation.PASSWORD); + bob.setCredentials(List.of(credentials)); + bob.setGroups(Arrays.asList("/employees", "/employees/devs")); + bob.setEmailVerified(true); + bob.setEnabled(true); + theRealm.users().create(bob); + + UserRepresentation john = new UserRepresentation(); + john.setEmail("john@acme.org"); + john.setUsername("john"); + john.setFirstName("John"); + john.setLastName("Smith"); + credentials = new CredentialRepresentation(); + credentials.setValue("john"); + credentials.setTemporary(false); + credentials.setType(CredentialRepresentation.PASSWORD); + john.setCredentials(List.of(credentials)); + john.setGroups(Arrays.asList("/employees", "/employees/sales")); + john.setEmailVerified(true); + john.setEnabled(true); + theRealm.users().create(john); + + // Client + ClientRepresentation jenkinsClient = new ClientRepresentation(); + jenkinsClient.setClientId(CLIENT); + jenkinsClient.setProtocol("openid-connect"); + jenkinsClient.setSecret(CLIENT); + final String jenkinsUrl = jenkins.url.toString(); + jenkinsClient.setRootUrl(jenkinsUrl); + jenkinsClient.setRedirectUris(List.of(String.format("%ssecurityRealm/finishLogin", jenkinsUrl))); + jenkinsClient.setWebOrigins(List.of(jenkinsUrl)); + jenkinsClient.setAttributes(Map.of("post.logout.redirect.uris", String.format("%sOicLogout", jenkinsUrl))); + theRealm.clients().create(jenkinsClient); + + // Assert that the realm is properly created + assertThat("group is created", theRealm.groups().groups().get(0).getName(), is("employees")); + GroupResource g = theRealm.groups().group(groupId); + assertThat("subgroups are created", + g.getSubGroups(0, 2, true).stream().map(GroupRepresentation::getName).collect(Collectors.toList()), + containsInAnyOrder("devs", "sales")); + assertThat("users are created", theRealm.users().list().stream().map(UserRepresentation::getUsername).collect(Collectors.toList()), + containsInAnyOrder("bob", "john")); + userBobKeycloakId = theRealm.users().searchByUsername("bob", true).get(0).getId(); + assertThat("User bob with the correct groups", + theRealm.users().get(userBobKeycloakId).groups().stream().map(GroupRepresentation::getPath).collect(Collectors.toList()), + containsInAnyOrder("/employees", "/employees/devs")); + userJohnKeycloakId = theRealm.users().searchByUsername("john", true).get(0).getId(); + assertThat("User john with the correct groups", + theRealm.users().get(userJohnKeycloakId).groups().stream().map(GroupRepresentation::getPath).collect(Collectors.toList()), + containsInAnyOrder("/employees", "/employees/sales")); + assertThat("client is created", + theRealm.clients().findByClientId(CLIENT).get(0).getProtocol(), is("openid-connect")); + } + } + + private void configureRealm() { + final String keycloakUrl = keycloak.getAuthServerUrl(); + GlobalSecurityConfig sc = new GlobalSecurityConfig(jenkins); + sc.open(); + OicAuthSecurityRealm securityRealm = sc.useRealm(OicAuthSecurityRealm.class); + securityRealm.configureClient(CLIENT, CLIENT); + securityRealm.setAutomaticConfiguration(String.format("%s/realms/%s/.well-known/openid-configuration", keycloakUrl, REALM)); + securityRealm.logoutFromOpenidProvider(true); + securityRealm.setPostLogoutUrl(jenkins.url("OicLogout").toExternalForm()); + sc.useAuthorizationStrategy(LoggedInAuthorizationStrategy.class); + sc.save(); + } + + @Test + public void fromJenkinsToKeycloak() { + final KeycloakUtils.User bob = new KeycloakUtils.User(userBobKeycloakId, "bob", "bob@acme.org", "Bob", "Smith"); + final KeycloakUtils.User john = new KeycloakUtils.User(userJohnKeycloakId, "john", "john@acme.org", "John", "Smith"); + jenkins.open(); + + jenkins.clickLink("log in"); + keycloakUtils.login(bob.getUserName()); + assertLoggedUser(bob); + + jenkins.logout(); + jenkins.open(); + assertLoggedOut(); + + // logout from Jenkins does mean logout from keycloak + jenkins.open(); + + clickLink("log in"); + keycloakUtils.login(john.getUserName()); + assertLoggedUser(john); + } + + @Test + public void fromKeycloakToJenkins() throws Exception { + final KeycloakUtils.User bob = new KeycloakUtils.User(userBobKeycloakId, "bob", "bob@acme.org", "Bob", "Smith"); + final KeycloakUtils.User john = new KeycloakUtils.User(userJohnKeycloakId, "john", "john@acme.org", "John", "Smith"); + final String loginUrl = String.format("%s/realms/%s/account", keycloak.getAuthServerUrl(), REALM); + keycloakUtils.open(new URL(loginUrl)); + + keycloakUtils.login(bob.getUserName()); + jenkins.open(); + jenkins.clickLink("log in"); // won't request a login, but log in directly with user from + + assertLoggedUser(bob); + + keycloakUtils.logout(bob); + jenkins.open(); + jenkins.logout(); // logout from keycloak does not logout from Jenkins (seems not supported in the plugin) + assertLoggedOut(); + + // Once logged out, we can change the user + jenkins.open(); + jenkins.clickLink("log in"); + keycloakUtils.login(john.getUserName()); + assertLoggedUser(john); + } + + private void assertLoggedOut() { + assertNull("User has logged out from Jenkins", jenkins.getCurrentUser().id()); + + assertThrows("User has logged out from keycloak", NoSuchElementException.class, + () -> keycloakUtils.getUser(keycloak.getAuthServerUrl(), REALM)); + } + + private void assertLoggedUser(KeycloakUtils.User expectedUser) { + assertThat("User has logged in Jenkins", jenkins.getCurrentUser().id(), is(expectedUser.getId())); + + KeycloakUtils.User fromKeyCloak = keycloakUtils.getUser(keycloak.getAuthServerUrl(), REALM); + assertThat("User has logged in keycloack", fromKeyCloak.getUserName(), is(expectedUser.getUserName())); + assertThat("User has logged in keycloack", fromKeyCloak.getEmail(), is(expectedUser.getEmail())); + assertThat("User has logged in keycloack", fromKeyCloak.getFirstName(), is(expectedUser.getFirstName())); + assertThat("User has logged in keycloack", fromKeyCloak.getLastName(), is(expectedUser.getLastName())); + } + +}