From 7e13146c96ab607301ee1993c4183569a93da0f7 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Thu, 7 Dec 2017 02:46:32 +0100 Subject: [PATCH] [JENKINS-47113] Populate the authorities after a successful authentication to Github (#87) This change stores a GitHub token in a user property for reuse by other authorization method. Specifically, the token in which the user authorized for Jenkins to collect consenting through OAuth. --- .../plugins/GithubAccessTokenProperty.java | 66 +++ .../plugins/GithubAuthenticationToken.java | 4 +- .../plugins/GithubSecretStorage.java | 62 +++ .../plugins/GithubSecurityRealm.java | 30 +- .../GithubAccessTokenPropertyTest.java | 388 ++++++++++++++++++ .../plugins/GithubSecretStorageTest.java | 101 +++++ .../jenkinsci/plugins/api/GihubAPITest.java | 1 + 7 files changed, 649 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/GithubAccessTokenProperty.java create mode 100644 src/main/java/org/jenkinsci/plugins/GithubSecretStorage.java create mode 100644 src/test/java/org/jenkinsci/plugins/GithubAccessTokenPropertyTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/GithubSecretStorageTest.java diff --git a/src/main/java/org/jenkinsci/plugins/GithubAccessTokenProperty.java b/src/main/java/org/jenkinsci/plugins/GithubAccessTokenProperty.java new file mode 100644 index 00000000..c590a5cd --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/GithubAccessTokenProperty.java @@ -0,0 +1,66 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins; + +import hudson.Extension; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import hudson.util.Secret; +import org.jenkinsci.Symbol; + +import javax.annotation.Nonnull; + +/** + * Remembers the access token used to connect to the Github server + * + * @since TODO + */ +public class GithubAccessTokenProperty extends UserProperty { + private final Secret accessToken; + + public GithubAccessTokenProperty(String accessToken) { + this.accessToken = Secret.fromString(accessToken); + } + + public @Nonnull Secret getAccessToken() { + return accessToken; + } + + @Extension + @Symbol("githubAccessToken") + public static final class DescriptorImpl extends UserPropertyDescriptor { + @Override + public boolean isEnabled() { + // does not show elements in //configure/ + return false; + } + + @Override + public UserProperty newInstance(User user) { + // no default property + return null; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java b/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java index 55f40c18..7e3b0c44 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java +++ b/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java @@ -412,6 +412,8 @@ public GithubOAuthUserDetails getUserDetails(String username) throws IOException public GrantedAuthority[] getGrantedAuthorities(GHUser user) { List groups = new ArrayList(); + groups.add(SecurityRealm.AUTHENTICATED_AUTHORITY); + try { GHPersonSet orgs; if(myRealm == null) { @@ -444,7 +446,7 @@ public GrantedAuthority[] getGrantedAuthorities(GHUser user) { GHTeam team = entry.getValue(); if (team.hasMember(user)) { groups.add(new GrantedAuthorityImpl(orgLogin + GithubOAuthGroupDetails.ORG_TEAM_SEPARATOR - + team)); + + team.getName())); } } } catch (IOException | Error ignore) { diff --git a/src/main/java/org/jenkinsci/plugins/GithubSecretStorage.java b/src/main/java/org/jenkinsci/plugins/GithubSecretStorage.java new file mode 100644 index 00000000..83bf3406 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/GithubSecretStorage.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins; + +import hudson.model.User; +import org.jfree.util.Log; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.IOException; + +public class GithubSecretStorage { + + private GithubSecretStorage(){ + // no accessible constructor + } + + public static boolean contains(@Nonnull User user) { + return user.getProperty(GithubAccessTokenProperty.class) != null; + } + + public static @CheckForNull String retrieve(@Nonnull User user) { + GithubAccessTokenProperty property = user.getProperty(GithubAccessTokenProperty.class); + if (property == null) { + Log.debug("Cache miss for username: " + user.getId()); + return null; + } else { + Log.debug("Token retrieved using cache for username: " + user.getId()); + return property.getAccessToken().getPlainText(); + } + } + + public static void put(@Nonnull User user, @Nonnull String accessToken) { + Log.debug("Populating the cache for username: " + user.getId()); + try { + user.addProperty(new GithubAccessTokenProperty(accessToken)); + } catch (IOException e) { + Log.warn("Received an exception when trying to add the GitHub access token to the user: " + user.getId(), e); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java b/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java index 6f847442..a046f51e 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java +++ b/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java @@ -379,6 +379,9 @@ public HttpResponse doFinishLogin(StaplerRequest request) if (u == null) { throw new IllegalStateException("Can't find user"); } + + GithubSecretStorage.put(u, accessToken); + u.setFullName(self.getName()); // Set email from github only if empty if (!u.getProperty(Mailer.UserProperty.class).hasExplicitlyConfiguredAddress()) { @@ -398,6 +401,10 @@ public HttpResponse doFinishLogin(StaplerRequest request) } SecurityListener.fireAuthenticated(new GithubOAuthUserDetails(self.getLogin(), auth.getAuthorities())); + + // While LastGrantedAuthorities are triggered by that event, we cannot trigger it there + // or modifications in organizations will be not reflected when using API Token, due to that caching + // SecurityListener.fireLoggedIn(self.getLogin()); } else { Log.info("Github did not return an access token."); } @@ -477,6 +484,14 @@ public Authentication authenticate(Authentication authentication) UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; GithubAuthenticationToken github = new GithubAuthenticationToken(token.getCredentials().toString(), getGithubApiUri()); SecurityContextHolder.getContext().setAuthentication(github); + + User user = User.getById(token.getName(), false); + if(user != null){ + GithubSecretStorage.put(user, token.getCredentials().toString()); + } + + SecurityListener.fireAuthenticated(new GithubOAuthUserDetails(token.getName(), github.getAuthorities())); + return github; } catch (IOException e) { throw new RuntimeException(e); @@ -621,10 +636,22 @@ public UserDetails loadUserByUsername(String username) throw new UsernameNotFoundException("Using org*team format instead of username: " + username); } + User localUser = User.getById(username, false); + Authentication token = SecurityContextHolder.getContext().getAuthentication(); if (token == null) { - throw new UserMayOrMayNotExistException("Could not get auth token."); + if(localUser != null && GithubSecretStorage.contains(localUser)){ + String accessToken = GithubSecretStorage.retrieve(localUser); + try { + token = new GithubAuthenticationToken(accessToken, getGithubApiUri()); + } catch (IOException e) { + throw new UserMayOrMayNotExistException("Could not connect to GitHub API server, target URL = " + getGithubApiUri(), e); + } + SecurityContextHolder.getContext().setAuthentication(token); + }else{ + throw new UserMayOrMayNotExistException("Could not get auth token."); + } } GithubAuthenticationToken authToken; @@ -639,7 +666,6 @@ public UserDetails loadUserByUsername(String username) * Always lookup the local user first. If we can't resolve it then we can burn an API request to Github for this user * Taken from hudson.security.HudsonPrivateSecurityRealm#loadUserByUsername(java.lang.String) */ - User localUser = User.getById(username, false); if (localUser != null) { return new GithubOAuthUserDetails(username, authToken); } diff --git a/src/test/java/org/jenkinsci/plugins/GithubAccessTokenPropertyTest.java b/src/test/java/org/jenkinsci/plugins/GithubAccessTokenPropertyTest.java new file mode 100644 index 00000000..a0c7ac23 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/GithubAccessTokenPropertyTest.java @@ -0,0 +1,388 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins; + +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebRequest; +import hudson.model.User; +import hudson.util.Scrambler; +import jenkins.security.ApiTokenProperty; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.xml.sax.SAXException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class GithubAccessTokenPropertyTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + private JenkinsRule.WebClient wc; + + private Server server; + private URI serverUri; + private MockGithubServlet servlet; + + public void setupMockGithubServer() throws Exception { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + // auto-bind to available port + connector.setPort(0); + server.addConnector(connector); + + servlet = new MockGithubServlet(j); + + ServletContextHandler context = new ServletContextHandler(); + ServletHolder servletHolder = new ServletHolder("default", servlet); + context.addServlet(servletHolder, "/*"); + server.setHandler(context); + + server.start(); + + String host = connector.getHost(); + if (host == null) { + host = "localhost"; + } + + int port = connector.getLocalPort(); + serverUri = new URI(String.format("http://%s:%d/", host, port)); + servlet.setServerUrl(serverUri); + } + + /** + * Based on documentation found at + * https://developer.github.com/v3/users/ + * https://developer.github.com/v3/orgs/ + * https://developer.github.com/v3/orgs/teams/ + */ + private static class MockGithubServlet extends DefaultServlet { + private String currentLogin; + private List organizations; + private List teams; + + private JenkinsRule jenkinsRule; + private URI serverUri; + + public MockGithubServlet(JenkinsRule jenkinsRule) { + this.jenkinsRule = jenkinsRule; + } + + public void setServerUrl(URI serverUri) { + this.serverUri = serverUri; + } + + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + switch (req.getRequestURI()) { + case "/user": + this.onUser(req, resp); + break; + case "/users/_specific_login_": + this.onUser(req, resp); + break; + case "/user/orgs": + this.onUserOrgs(req, resp); + break; + case "/user/teams": + this.onUserTeams(req, resp); + break; + case "/orgs/org-a": + this.onOrgs(req, resp, "org-a"); + break; + case "/orgs/org-a/teams": + this.onOrgsTeam(req, resp, "org-a"); + break; + case "/orgs/org-a/members/alice": + this.onOrgsMember(req, resp, "org-a", "alice"); + break; + case "/teams/7/members/alice": + this.onTeamMember(req, resp, "team-b", "alice"); + break; + case "/orgs/org-c": + this.onOrgs(req, resp, "org-c"); + break; + case "/orgs/org-c/teams": + this.onOrgsTeam(req, resp, "org-c"); + break; + case "/orgs/org-c/members/bob": + this.onOrgsMember(req, resp, "org-c", "bob"); + break; + case "/teams/7/members/bob": + this.onTeamMember(req, resp, "team-d", "bob"); + break; + case "/login/oauth/authorize": + this.onLoginOAuthAuthorize(req, resp); + break; + case "/login/oauth/access_token": + this.onLoginOAuthAccessToken(req, resp); + break; + default: + throw new RuntimeException("Url not mapped yet: " + req.getRequestURI()); + } + } + + private void onUser(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.getWriter().write(JSONObject.fromObject( + new HashMap() {{ + put("login", currentLogin); + put("name", currentLogin + "_name"); + // to avoid triggering a second call, due to GithubSecurityRealm:382 + put("created_at", "2008-01-14T04:33:35Z"); + put("url", serverUri + "/users/_specific_login_"); + }} + ).toString()); + } + + private void onUserOrgs(HttpServletRequest req, HttpServletResponse resp) throws IOException { + List> responseBody = new ArrayList<>(); + for (String orgName : organizations) { + final String orgName_ = orgName; + responseBody.add(new HashMap() {{ + put("login", orgName_); + }}); + } + + resp.getWriter().write(JSONArray.fromObject(responseBody).toString()); + } + + private void onOrgs(HttpServletRequest req, HttpServletResponse resp, final String orgName) throws IOException { + Map responseBody = new HashMap() {{ + put("login", orgName); + }}; + + resp.getWriter().write(JSONObject.fromObject(responseBody).toString()); + } + + private void onOrgsMember(HttpServletRequest req, HttpServletResponse resp, String orgName, String userName) throws IOException { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + // 302 / 404 responses not implemented + } + + private void onTeamMember(HttpServletRequest req, HttpServletResponse resp, String orgName, String userName) throws IOException { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + // 302 / 404 responses not implemented + } + + private void onOrgsTeam(HttpServletRequest req, HttpServletResponse resp, final String orgName) throws IOException { + List> responseBody = new ArrayList<>(); + for (String teamName : teams) { + final String teamName_ = teamName; + responseBody.add(new HashMap() {{ + put("id", 7); + put("login", teamName_ + "_login"); + put("name", teamName_); + put("organization", new HashMap() {{ + put("login", orgName); + }}); + }}); + } + + resp.getWriter().write(JSONArray.fromObject(responseBody).toString()); + } + + private void onUserTeams(HttpServletRequest req, HttpServletResponse resp) throws IOException { + List> responseBody = new ArrayList<>(); + for (String teamName : teams) { + final String teamName_ = teamName; + responseBody.add(new HashMap() {{ + put("login", teamName_ + "_login"); + put("name", teamName_); + put("organization", new HashMap() {{ + put("login", organizations.get(0)); + }}); + }}); + } + + resp.getWriter().write(JSONArray.fromObject(responseBody).toString()); + } + + private void onLoginOAuthAuthorize(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String code = "test"; + resp.sendRedirect(jenkinsRule.getURL() + "securityRealm/finishLogin?code=" + code); + } + + private void onLoginOAuthAccessToken(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.getWriter().write("access_token=RANDOM_ACCESS_TOKEN"); + } + } + + @Before + public void prepareRealmAndWebClient() throws Exception { + this.setupMockGithubServer(); + this.setupRealm(); + wc = j.createWebClient(); + } + + private void setupRealm(){ + String githubWebUri = serverUri.toString(); + String githubApiUri = serverUri.toString(); + String clientID = "xxx"; + String clientSecret = "yyy"; + String oauthScopes = "read:org"; + + GithubSecurityRealm githubSecurityRealm = new GithubSecurityRealm( + githubWebUri, + githubApiUri, + clientID, + clientSecret, + oauthScopes + ); + + j.jenkins.setSecurityRealm(githubSecurityRealm); + } + + @After + public void stopEmbeddedJettyServer() { + try { + server.stop(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Issue("JENKINS-47113") + @Test + public void testUsingGithubToken() throws IOException, SAXException { + String aliceLogin = "alice"; + servlet.currentLogin = aliceLogin; + servlet.organizations = Arrays.asList("org-a"); + servlet.teams = Arrays.asList("team-b"); + + User aliceUser = User.getById(aliceLogin, true); + String aliceApiRestToken = aliceUser.getProperty(ApiTokenProperty.class).getApiToken(); + String aliceGitHubToken = "SPECIFIC_TOKEN"; + + // request whoAmI with ApiRestToken => group not populated + makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceApiRestToken), "alice", Arrays.asList("authenticated")); + + // request whoAmI with GitHubToken => group populated + makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceGitHubToken), "alice", Arrays.asList("authenticated", "org-a", "org-a*team-b")); + + // no authentication in session but use the cache + makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceApiRestToken), "alice", Arrays.asList("authenticated", "org-a", "org-a*team-b")); + + wc = j.createWebClient(); + // no session at all, use the cache also + makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceApiRestToken), "alice", Arrays.asList("authenticated", "org-a", "org-a*team-b")); + } + + @Issue("JENKINS-47113") + @Test + public void testUsingGithubLogin() throws IOException, SAXException { + String bobLogin = "bob"; + servlet.currentLogin = bobLogin; + servlet.organizations = Arrays.asList("org-c"); + servlet.teams = Arrays.asList("team-d"); + + User bobUser = User.getById(bobLogin, true); + String bobApiRestToken = bobUser.getProperty(ApiTokenProperty.class).getApiToken(); + + // request whoAmI with ApiRestToken => group not populated + makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated")); + // request whoAmI with GitHub OAuth => group populated + makeRequestUsingOAuth("bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); + + // use only the session + // request whoAmI with ApiRestToken => group populated (due to login event) + makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); + + wc = j.createWebClient(); + // retrieve the security group even without the cookie (using LastGrantedAuthorities this time) + makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); + } + + private void makeRequestWithAuthCodeAndVerify(String authCode, String expectedLogin, List expectedAuthorities) throws IOException, SAXException { + WebRequest req = new WebRequest(new URL(j.getURL(), "whoAmI/api/json")); + req.setEncodingType(null); + if (authCode != null) + req.setAdditionalHeader("Authorization", authCode); + Page p = wc.getPage(req); + + assertResponse(p, expectedLogin, expectedAuthorities); + } + + private void makeRequestUsingOAuth(String expectedLogin, List expectedAuthorities) throws IOException { + WebRequest req = new WebRequest(new URL(j.getURL(), "securityRealm/commenceLogin")); + req.setEncodingType(null); + + String referer = j.getURL() + "whoAmI/api/json"; + req.setAdditionalHeader("Referer", referer); + Page p = wc.getPage(req); + + assertResponse(p, expectedLogin, expectedAuthorities); + } + + private void assertResponse(Page p, String expectedLogin, List expectedAuthorities) { + String response = p.getWebResponse().getContentAsString().trim(); + JSONObject respObject = JSONObject.fromObject(response); + if (expectedLogin != null) { + assertEquals(expectedLogin, respObject.getString("name")); + } + if (expectedAuthorities != null) { + // we use set to avoid having duplicated "authenticated" + // as that will be corrected in https://github.com/jenkinsci/jenkins/pull/3123 + Set actualAuthorities = new HashSet<>( + JSONArray.toCollection( + respObject.getJSONArray("authorities"), + String.class + ) + ); + + Set expectedAuthoritiesSet = new HashSet<>(expectedAuthorities); + + assertTrue(String.format("They do not have the same content, expected=%s, actual=%s", expectedAuthorities, actualAuthorities), + expectedAuthoritiesSet.equals(actualAuthorities)); + } + } + + private String encodeBasic(String login, String credentials) { + return "Basic " + Scrambler.scramble(login + ":" + credentials); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/GithubSecretStorageTest.java b/src/test/java/org/jenkinsci/plugins/GithubSecretStorageTest.java new file mode 100644 index 00000000..f877d1b9 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/GithubSecretStorageTest.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins; + +import hudson.model.User; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class GithubSecretStorageTest { + + @Rule + public RestartableJenkinsRule j = new RestartableJenkinsRule(); + + @Test + public void correctBehavior() throws Exception { + j.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + User.getById("alice", true); + User.getById("bob", true); + + String secret = "$3cR3t"; + + Assert.assertFalse(GithubSecretStorage.contains(retrieveUser())); + Assert.assertNull(GithubSecretStorage.retrieve(retrieveUser())); + + Assert.assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); + + GithubSecretStorage.put(retrieveUser(), secret); + + Assert.assertTrue(GithubSecretStorage.contains(retrieveUser())); + Assert.assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); + + Assert.assertEquals(secret, GithubSecretStorage.retrieve(retrieveUser())); + } + }); + } + + private User retrieveUser() { + return User.getById("alice", false); + } + + private User retrieveOtherUser() { + return User.getById("bob", false); + } + + @Test + public void correctBehaviorEvenAfterRestart() throws Exception { + final String secret = "$3cR3t"; + + j.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + User.getById("alice", true).save(); + User.getById("bob", true).save(); + + Assert.assertFalse(GithubSecretStorage.contains(retrieveUser())); + Assert.assertNull(GithubSecretStorage.retrieve(retrieveUser())); + + Assert.assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); + + GithubSecretStorage.put(retrieveUser(), secret); + + Assert.assertTrue(GithubSecretStorage.contains(retrieveUser())); + Assert.assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); + + Assert.assertEquals(secret, GithubSecretStorage.retrieve(retrieveUser())); + } + }); + j.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + Assert.assertTrue(GithubSecretStorage.contains(retrieveUser())); + Assert.assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); + + Assert.assertEquals(secret, GithubSecretStorage.retrieve(retrieveUser())); + } + }); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/api/GihubAPITest.java b/src/test/java/org/jenkinsci/plugins/api/GihubAPITest.java index 0226e013..33422068 100644 --- a/src/test/java/org/jenkinsci/plugins/api/GihubAPITest.java +++ b/src/test/java/org/jenkinsci/plugins/api/GihubAPITest.java @@ -38,6 +38,7 @@ of this software and associated documentation files (the "Software"), to deal import org.kohsuke.github.GHUser; import org.kohsuke.github.GitHub; +//TODO could use JUnit Assume.* instead of @Ignore /** * @author mocleiri *