Skip to content
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

[JENKINS-49486] Enable variable expansion for ManualCondition.users (approvers) #114

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public boolean canPromote(String processName) {
if (process != null) {
manualCondition = (ManualCondition) process.getPromotionCondition(ManualCondition.class.getName());
}
return PromotionPermissionHelper.hasPermission(getProject(), manualCondition);
return PromotionPermissionHelper.hasPermission(getProject(), owner, manualCondition);
}

/**
Expand Down Expand Up @@ -239,7 +239,7 @@ public HttpResponse doForcePromotion(@QueryParameter("name") String name) throws
throw new IllegalStateException("This project doesn't have the promotion criterion called "+name);

ManualCondition manualCondition = (ManualCondition) p.getPromotionCondition(ManualCondition.class.getName());
PromotionPermissionHelper.checkPermission(getProject(), manualCondition);
PromotionPermissionHelper.checkPermission(getProject(), owner, manualCondition);

p.promote(owner,new UserCause(),new ManualPromotionBadge());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

package hudson.plugins.promoted_builds;

import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.plugins.promoted_builds.conditions.ManualCondition;
import hudson.security.AccessDeniedException2;
Expand All @@ -40,19 +41,19 @@
@Restricted(NoExternalUse.class)
public class PromotionPermissionHelper {

public static void checkPermission(@Nonnull AbstractProject<?,?> target, @CheckForNull ManualCondition associatedCondition) {
if (!hasPermission(target, associatedCondition)) {
public static void checkPermission(@Nonnull AbstractProject<?,?> target, @Nonnull AbstractBuild<?,?> build, @CheckForNull ManualCondition associatedCondition) {
if (!hasPermission(target, build, associatedCondition)) {
// TODO: Give a more accurate error message if the user has Promotion.PROMOTE but is not in the list of approvers.
throw new AccessDeniedException2(Jenkins.getAuthentication(), Promotion.PROMOTE);
}
}

public static boolean hasPermission(@Nonnull AbstractProject<?,?> target, @CheckForNull ManualCondition associatedCondition) {
public static boolean hasPermission(@Nonnull AbstractProject<?,?> target, @Nonnull AbstractBuild<?,?> build, @CheckForNull ManualCondition associatedCondition) {
if (associatedCondition == null) {
return target.hasPermission(Promotion.PROMOTE);
} else if (associatedCondition.getUsersAsSet().isEmpty()) {
} else if (associatedCondition.getUsersAsSet(build).isEmpty()) {
return target.hasPermission(Promotion.PROMOTE);
} else if (associatedCondition.isInUsersList() || associatedCondition.isInGroupList()) {
} else if (associatedCondition.isInUsersList(build) || associatedCondition.isInGroupList(build)) {
// Explicitly listed users do not need Promotion/Promote permissions.
return true;
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/hudson/plugins/promoted_builds/Status.java
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ public boolean canBuild() {
}

ManualCondition manualCondition = (ManualCondition) process.getPromotionCondition(ManualCondition.class.getName());
return PromotionPermissionHelper.hasPermission(target.getProject(), manualCondition);
return PromotionPermissionHelper.hasPermission(target.getProject(), target, manualCondition);
}

/**
Expand All @@ -399,7 +399,7 @@ public void doBuild(StaplerRequest req, StaplerResponse rsp) throws IOException,

ManualCondition manualCondition = (ManualCondition) process.getPromotionCondition(ManualCondition.class.getName());
// TODO: Use PromotionPermissionHelper.checkPermission instead, but consider issues with backwards compatibility.
if (!PromotionPermissionHelper.hasPermission(target.getProject(), manualCondition)) {
if (!PromotionPermissionHelper.hasPermission(target.getProject(), target, manualCondition)) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

Expand All @@ -54,6 +56,7 @@
public class ManualCondition extends PromotionCondition {
private String users;
private List<ParameterDefinition> parameterDefinitions = new ArrayList<ParameterDefinition>();
private static final Logger LOGGER = Logger.getLogger(ManualCondition.class.getName());

/**
*
Expand Down Expand Up @@ -96,17 +99,31 @@ public ParameterDefinition getParameterDefinition(String name) {
}

public Set<String> getUsersAsSet() {
if (users == null || users.equals(""))
return getUsersAsSet(null);
}

public Set<String> getUsersAsSet(AbstractBuild<?,?> build) {
Copy link
Member

@dwnusbaum dwnusbaum Aug 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaks binary compatibility, although I don't see any other callers based on a GitHub search of the jenkinsci organization, so it might not matter. We could always be safe and keep the previous method around as well, and have it call the new method passing null for the new parameter.

Same issue with isInUsersList and isInGroupList .

if (users == null || users.equals("") || build == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is not correct from what I see.
When build is null, the result will be always empty.
E.g. getUsersAsSet() will be always returning empty list after the change

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What sensitive environment variable is exposing right now?
Could you give me some examples?

return Collections.emptySet();

Set<String> set = new HashSet<String>();
for (String user : users.split(",")) {
user = user.trim();

if (user.trim().length() > 0)
LOGGER.log(Level.FINER, "ManualCondition.getUsersAsSet(): user={0}", user);
if (user.startsWith("$")){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need to null-check build somewhere before using it below. I think it makes sense to do it here: if (build != null && user.startsWith("$")). That way if the the build is null, we don't try to substitute parameterized names, and just treat them as actual usernames.

user = user.substring(1);
if (user.startsWith("{") && user.endsWith("}"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to allow either $NAME or ${NAME}, but not both. What happens if there is a user named $NAME? I don't think users can have $ in their name after a recent security fix, but I am not 100% sure.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason I allow both $NAME and ${NAME} because
Email-ext plugin and token-macro plugin support these 2 syntax.

But if you insist,
I could allow only one of the case,
and which form do you prefert to support?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If email-ext and token-macro support both, then it probably makes sense to support both here as well. I'd leave it the way you have it for now.

user = user.substring(1, user.length() -1);
LOGGER.log(Level.INFO, "ManualCondition.getUsersAsSet(): build.getBuildVariableResolver().resolve(user=${0})={1}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Level.INFO is visible by default in Jenkins, so I would move this and the other call to LOGGER.log to Level.FINER or similar.

new Object [] {user, build.getBuildVariableResolver().resolve(user)});
user = build.getBuildVariableResolver().resolve(user);
}
if (user != null && user.trim().length() > 0)
set.add(user);
}

LOGGER.log(Level.INFO, "ManualCondition.getUsersAsSet(): return set={0}", set);
return set;
}

Expand All @@ -129,7 +146,7 @@ public PromotionBadge isMet(PromotionProcess promotionProcess, AbstractBuild<?,?
* approved.
*/
public boolean canApprove(PromotionProcess promotionProcess, AbstractBuild<?,?> build) {
if (!PromotionPermissionHelper.hasPermission(build.getProject(), this)) {
if (!PromotionPermissionHelper.hasPermission(build.getProject(), build, this)) {
return false;
}

Expand All @@ -150,16 +167,24 @@ public boolean canApprove(PromotionProcess promotionProcess, AbstractBuild<?,?>
* @since 2.24
*/
public boolean isInUsersList() {
return isInUsersList(null);
}

public boolean isInUsersList(AbstractBuild<?,?> build) {
// Current user must be in users list or users list is empty
Set<String> usersSet = getUsersAsSet();
Set<String> usersSet = getUsersAsSet(build);
return usersSet.contains(Hudson.getAuthentication().getName());
}

/*
* Check if user is a member of a groups as listed in the user / group field
*/
public boolean isInGroupList() {
Set<String> groups = getUsersAsSet();
return isInGroupList(null);
}

public boolean isInGroupList(AbstractBuild<?,?> build) {
Set<String> groups = getUsersAsSet(build);
GrantedAuthority[] authorities = Hudson.getAuthentication().getAuthorities();
for (GrantedAuthority authority : authorities) {
if (groups.contains(authority.getAuthority()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<f:form method="post" action="promotionProcess/${h.encode(p.name)}/promotionCondition/hudson.plugins.promoted_builds.conditions.ManualCondition/approve" name="approve">
<j:if test="${!it.usersAsSet.isEmpty()}">
<f:entry title="${%Approvers}" description="${%List of users or groups that can approve this promotion}">
<f:textbox value="${it.users}" readonly="true"/>
<f:textbox value="${it.getUsersAsSet(pba.owner)}" readonly="true"/>
</f:entry>
</j:if>
<j:if test="${it.canApprove(p, pba.owner)}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

import hudson.ExtensionList;
import hudson.model.AbstractProject;
import hudson.model.Cause;
import hudson.model.FreeStyleBuild;
import hudson.model.Item;
import hudson.model.ParametersAction;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.Result;
import hudson.model.Descriptor;
import hudson.model.FreeStyleProject;
import hudson.model.StringParameterDefinition;
import hudson.model.StringParameterValue;
import hudson.model.User;
import hudson.plugins.promoted_builds.JobPropertyImpl;
import hudson.plugins.promoted_builds.PromotedBuildAction;
Expand Down Expand Up @@ -263,6 +266,25 @@ public void testManualPromotionPermissions() throws Exception {
approval, notNullValue());
}

{
// Approvers specified in varaible, user is approver, but does not have Promotion/Promote
cond.setUsers("${approver}");
List<ParameterValue> paramsList = new ArrayList<ParameterValue>();
paramsList.add(new StringParameterValue("approver", "non-promoter"));
//FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0));
FreeStyleBuild b = j.assertBuildStatusSuccess(
p.scheduleBuild2(
0,
new Cause.RemoteCause("test-host", ""),
new ParametersAction(paramsList)));
SecurityContext previous = ACL.impersonate(User.get("non-promoter").impersonate());
j.assertBuildStatusSuccess(cond.approve(b, pp, Collections.EMPTY_LIST));
ACL.impersonate(previous.getAuthentication());
ManualApproval approval = b.getAction(ManualApproval.class);
assertThat("If users are specified in variable, then users in that list should be able to approve even without Promotion/Promote permissions",
approval, notNullValue());
}

{
// Approvers specified, user is not approver, but does have Promotion/Promote
cond.setUsers("non-promoter");
Expand All @@ -283,7 +305,14 @@ public void testManualPromotionPermissionsViaWebClient() throws Exception {
PromotionProcess pp = addPromotionProcess(p, "foo");
WebClient wc = j.createWebClient();

FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0));
List<ParameterValue> paramsList = new ArrayList<ParameterValue>();
paramsList.add(new StringParameterValue("approver", "promoter"));
//FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0));
FreeStyleBuild b = j.assertBuildStatusSuccess(
p.scheduleBuild2(
0,
new Cause.RemoteCause("test-host", ""),
new ParametersAction(paramsList)));
ManualCondition cond = new ManualCondition();
pp.conditions.add(cond);
j.assertBuildStatusSuccess(cond.approve(b, pp, Collections.EMPTY_LIST));
Expand Down Expand Up @@ -322,13 +351,26 @@ public void testManualPromotionPermissionsViaWebClient() throws Exception {
assertThat(waitForBuildByNumber(pp, 3).getResult(), equalTo(Result.SUCCESS));
}

{
// Re-execute promotion as specified user in varaible without Promotion/Promote
cond.setUsers("${approver}");
wc.login("non-promoter", "non-promoter");
try {
wc.getPage(b, String.format("promotion/%s/build?json={}", pp.getName()));
fail();
} catch (FailingHttpStatusCodeException e) {
assertThat(e.getStatusCode(), equalTo(404)); // Redirect after the build is broken.
}
assertThat(waitForBuildByNumber(pp, 4).getResult(), equalTo(Result.SUCCESS));
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwnusbaum
Do you know why this webclient test fail?

Copy link
Member

@dwnusbaum dwnusbaum Aug 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a glance, no, but you could use a debugger and step through the test/plugin code to figure out why it is failing before removing it :)

{
// Re-execute promotion as unspecified user with Promotion/Promote
cond.setUsers("non-promoter");
wc.login("promoter", "promoter");
// Status#doBuild does a bare `return;` without scheduling the build in this case, which is why we use goTo with "" for the MIME type.
wc.goTo(String.format("job/%s/%d/promotion/%s/build?json={}", p.getName(), b.getNumber(), pp.getName()), "");
assertThat(pp.getBuildByNumber(4), nullValue());
assertThat(pp.getBuildByNumber(5), nullValue());
}
}

Expand Down