Skip to content

Add Jenkins agent support for GitHub Committer Authorization Strategy #209

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

Merged
merged 9 commits into from
Jul 31, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ of this software and associated documentation files (the "Software"), to deal
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -140,6 +141,23 @@ public String getAdminUserNames() {
return StringUtils.join(rootACL.getAdminUserNameList().iterator(), ", ");
}

/** Set the agent username. We use a setter instead of a constructor to make this an optional field
* to avoid a breaking change.
* @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#setAgentUserName(String)
*/
@DataBoundSetter
public void setAgentUserName(String agentUserName) {
rootACL.setAgentUserName(agentUserName);
}

/**
* @return agentUserName
* @see GithubRequireOrganizationMembershipACL#getAgentUserName()
*/
public String getAgentUserName() {
return rootACL.getAgentUserName();
}

/**
* @return isUseRepositoryPermissions
* @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isUseRepositoryPermissions()
Expand Down Expand Up @@ -208,6 +226,7 @@ public boolean equals(Object object){
GithubAuthorizationStrategy obj = (GithubAuthorizationStrategy) object;
return this.getOrganizationNames().equals(obj.getOrganizationNames()) &&
this.getAdminUserNames().equals(obj.getAdminUserNames()) &&
this.getAgentUserName().equals(obj.getAgentUserName()) &&
this.isUseRepositoryPermissions() == obj.isUseRepositoryPermissions() &&
this.isAuthenticatedUserCreateJobPermission() == obj.isAuthenticatedUserCreateJobPermission() &&
this.isAuthenticatedUserReadPermission() == obj.isAuthenticatedUserReadPermission() &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ of this software and associated documentation files (the "Software"), to deal
*/
package org.jenkinsci.plugins;

import hudson.model.*;
import org.acegisecurity.Authentication;
import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
Expand All @@ -41,10 +42,6 @@ of this software and associated documentation files (the "Software"), to deal
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

import hudson.model.AbstractItem;
import hudson.model.AbstractProject;
import hudson.model.Describable;
import hudson.model.Item;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.UserRemoteConfig;
import hudson.security.ACL;
Expand All @@ -64,6 +61,7 @@ public class GithubRequireOrganizationMembershipACL extends ACL {

private final List<String> organizationNameList;
private final List<String> adminUserNameList;
private String agentUserName;
private final boolean authenticatedUserReadPermission;
private final boolean useRepositoryPermissions;
private final boolean authenticatedUserCreateJobPermission;
Expand Down Expand Up @@ -102,6 +100,12 @@ public boolean hasPermission(@NonNull Authentication a, @NonNull Permission perm
return true;
}

// Grant agent permissions to agent user
if (candidateName.equalsIgnoreCase(agentUserName) && checkAgentUserPermission(permission)) {
log.finest("Granting Agent Connect rights to user " + candidateName);
return true;
}

// Are they trying to read?
if (checkReadPermission(permission)) {
// if we support authenticated read return early
Expand Down Expand Up @@ -153,6 +157,12 @@ else if (testBuildPermission(permission) && isInWhitelistedOrgs(authenticationTo
return true;
}

// Grant agent permissions to agent user
if (authenticatedUserName.equalsIgnoreCase(agentUserName) && checkAgentUserPermission(permission)) {
log.finest("Granting Agent Connect rights to user " + authenticatedUserName);
return true;
}

if (authenticatedUserName.equals("anonymous")) {
if (checkJobStatusPermission(permission) && allowAnonymousJobStatusPermission) {
return true;
Expand Down Expand Up @@ -239,6 +249,13 @@ private boolean checkReadPermission(@NonNull Permission permission) {
|| id.equals("hudson.model.Item.Read"));
}

private boolean checkAgentUserPermission(@NonNull Permission permission) {
return permission.equals(Hudson.READ)
|| permission.equals(Computer.CREATE)
|| permission.equals(Computer.CONNECT)
|| permission.equals(Computer.CONFIGURE);
}

private boolean checkJobStatusPermission(@NonNull Permission permission) {
return permission.getId().equals("hudson.model.Item.ViewStatus");
}
Expand Down Expand Up @@ -314,10 +331,11 @@ public GithubRequireOrganizationMembershipACL(String adminUserNames,
}

this.item = null;
this.agentUserName = ""; // Initially blank - populated by a setter since this field is optional
}

public GithubRequireOrganizationMembershipACL cloneForProject(AbstractItem item) {
return new GithubRequireOrganizationMembershipACL(
GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL(
this.adminUserNameList,
this.organizationNameList,
this.authenticatedUserReadPermission,
Expand All @@ -328,6 +346,8 @@ public GithubRequireOrganizationMembershipACL cloneForProject(AbstractItem item)
this.allowAnonymousReadPermission,
this.allowAnonymousJobStatusPermission,
item);
acl.setAgentUserName(agentUserName);
return acl;
}

public GithubRequireOrganizationMembershipACL(List<String> adminUserNameList,
Expand Down Expand Up @@ -362,6 +382,11 @@ public List<String> getAdminUserNameList() {
return adminUserNameList;
}

public void setAgentUserName(String agentUserName) {
this.agentUserName = agentUserName;
}
public String getAgentUserName() { return agentUserName; }

public boolean isUseRepositoryPermissions() {
return useRepositoryPermissions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<f:textbox />
</f:entry>

<f:entry title="Agent User Name" field="agentUserName" help="/plugin/github-oauth/help/auth/agent-user-name-help.html" >
<f:textbox />
</f:entry>

<f:entry title="Participant in Organization" field="organizationNames" help="/plugin/github-oauth/help/auth/organization-names-help.html">
<f:textbox />
</f:entry>
Expand Down
3 changes: 3 additions & 0 deletions src/main/webapp/help/auth/agent-user-name-help.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
If you are using inbound Jenkins agents, this is the user that is used for authenticating agents. This user will receive rights to create, connect and configure agents.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.Collections;
import java.util.List;

import hudson.model.Computer;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.Messages;
Expand Down Expand Up @@ -130,7 +131,7 @@ private void mockJenkins(MockedStatic<Jenkins> mockedJenkins) {
new GrantedAuthority[]{new GrantedAuthorityImpl("anonymous")});

private GithubRequireOrganizationMembershipACL createACL() {
return new GithubRequireOrganizationMembershipACL(
GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL(
"admin",
"myOrg",
authenticatedUserReadPermission,
Expand All @@ -140,6 +141,8 @@ private GithubRequireOrganizationMembershipACL createACL() {
allowAnonymousCCTrayPermission,
allowAnonymousReadPermission,
allowAnonymousJobStatusPermission);
acl.setAgentUserName("agent");
return acl;
}

private GithubRequireOrganizationMembershipACL aclForProject(Project project) {
Expand Down Expand Up @@ -554,6 +557,30 @@ public void testCannotReadRepositoryWithInvalidRepoUrl() throws IOException {
}
}

@Test
public void testAgentUserCanCreateConnectAndConfigureAgents() {
GithubAuthenticationToken authenticationToken = Mockito.mock(GithubAuthenticationToken.class);
Mockito.when(authenticationToken.isAuthenticated()).thenReturn(true);
Mockito.when(authenticationToken.getName()).thenReturn("agent");
GithubRequireOrganizationMembershipACL acl = createACL();

assertTrue(acl.hasPermission(authenticationToken, Computer.CREATE));
assertTrue(acl.hasPermission(authenticationToken, Computer.CONFIGURE));
assertTrue(acl.hasPermission(authenticationToken, Computer.CONNECT));
}

@Test
public void testAuthenticatedCanNotCreateConnectAndConfigureAgents() {
GithubAuthenticationToken authenticationToken = Mockito.mock(GithubAuthenticationToken.class);
Mockito.when(authenticationToken.isAuthenticated()).thenReturn(true);
Mockito.when(authenticationToken.getName()).thenReturn("authenticated");
GithubRequireOrganizationMembershipACL acl = createACL();

assertFalse(acl.hasPermission(authenticationToken, Computer.CREATE));
assertFalse(acl.hasPermission(authenticationToken, Computer.CONFIGURE));
assertFalse(acl.hasPermission(authenticationToken, Computer.CONNECT));
}

@Test
public void testAnonymousCanViewJobStatusWhenGranted() {
this.allowAnonymousJobStatusPermission = true;
Expand Down