Skip to content

Commit 6172be4

Browse files
scherlerVlatombeDohbedoh
authored
[JENKINS-75545] Make pod templates more flexible configurable based on permissions (#1671)
Co-authored-by: Vincent Latombe <[email protected]> Co-authored-by: Allan Burdajewicz <[email protected]>
1 parent e5fbdce commit 6172be4

File tree

9 files changed

+142
-30
lines changed

9 files changed

+142
-30
lines changed

src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import hudson.model.Label;
2424
import hudson.security.ACL;
2525
import hudson.security.AccessControlled;
26+
import hudson.security.Permission;
2627
import hudson.slaves.Cloud;
2728
import hudson.slaves.NodeProvisioner;
2829
import hudson.util.FormApply;
@@ -737,10 +738,16 @@ private static void ensureServerCertificateInFipsMode(String serverCertificate)
737738

738739
@Override
739740
public void replaceTemplate(PodTemplate oldTemplate, PodTemplate newTemplate) {
741+
this.checkManagePermission();
740742
this.removeTemplate(oldTemplate);
741743
this.addTemplate(newTemplate);
742744
}
743745

746+
@Override
747+
public Permission getManagePermission() {
748+
return Jenkins.MANAGE;
749+
}
750+
744751
@Override
745752
public boolean canProvision(@NonNull Cloud.CloudState state) {
746753
return getTemplate(state.getLabel()) != null;
@@ -805,6 +812,7 @@ public List<PodTemplate> getTemplatesFor(@CheckForNull Label label) {
805812
*/
806813
@Override
807814
public void addTemplate(PodTemplate t) {
815+
this.checkManagePermission();
808816
this.templates.add(t);
809817
// t.parent = this;
810818
}
@@ -816,6 +824,7 @@ public void addTemplate(PodTemplate t) {
816824
*/
817825
@Override
818826
public void removeTemplate(PodTemplate t) {
827+
this.checkManagePermission();
819828
this.templates.remove(t);
820829
}
821830

@@ -920,7 +929,7 @@ public PodTemplate.DescriptorImpl getTemplateDescriptor() {
920929
public HttpResponse doCreate(StaplerRequest2 req, StaplerResponse2 rsp)
921930
throws IOException, ServletException, Descriptor.FormException {
922931
Jenkins j = Jenkins.get();
923-
j.checkPermission(Jenkins.MANAGE);
932+
this.checkManagePermission();
924933
PodTemplate newTemplate = getTemplateDescriptor().newInstance(req, req.getSubmittedForm());
925934
addTemplate(newTemplate);
926935
j.save();

src/main/java/org/csanchez/jenkins/plugins/kubernetes/NonConfigurableKubernetesCloud.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import jenkins.model.Jenkins;
1111
import net.sf.json.JSONObject;
1212
import org.kohsuke.stapler.StaplerRequest2;
13+
import org.springframework.security.access.AccessDeniedException;
1314

1415
public class NonConfigurableKubernetesCloud extends KubernetesCloud {
1516
public NonConfigurableKubernetesCloud(@NonNull String name, @NonNull KubernetesCloud source) {
@@ -25,6 +26,14 @@ public void addTemplate(PodTemplate template) {}
2526
@Override
2627
public void removeTemplate(PodTemplate template) {}
2728

29+
@Override
30+
public boolean hasManagePermission() {
31+
return false;
32+
}
33+
34+
@Override
35+
public void checkManagePermission() throws AccessDeniedException {}
36+
2837
@Override
2938
public Cloud reconfigure(@NonNull StaplerRequest2 req, JSONObject form) throws Descriptor.FormException {
3039
return DescriptorImpl.class.cast(getDescriptor()).newInstance(req, form);

src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.kohsuke.stapler.DataBoundSetter;
5454
import org.kohsuke.stapler.HttpRedirect;
5555
import org.kohsuke.stapler.HttpResponse;
56+
import org.kohsuke.stapler.Stapler;
5657
import org.kohsuke.stapler.StaplerRequest2;
5758
import org.kohsuke.stapler.verb.POST;
5859

@@ -661,16 +662,29 @@ public void addEnvVars(List<TemplateEnvVar> envVars) {
661662
}
662663
}
663664

665+
@SuppressWarnings("unused") // Used by jelly
666+
@Restricted(DoNotUse.class) // Used by jelly
667+
public boolean hasManagePermission() {
668+
StaplerRequest2 request = Stapler.getCurrentRequest2();
669+
if (request != null) {
670+
PodTemplateGroup groupFromRequest = request.findAncestorObject(PodTemplateGroup.class);
671+
if (groupFromRequest != null) {
672+
return groupFromRequest.hasManagePermission();
673+
}
674+
}
675+
return Jenkins.get().hasPermission(Jenkins.MANAGE);
676+
}
677+
664678
/**
665679
* Deletes the template.
666680
*/
667681
@POST
668682
public HttpResponse doDoDelete(@AncestorInPath PodTemplateGroup owner) throws IOException {
669-
Jenkins j = Jenkins.get();
670-
j.checkPermission(Jenkins.MANAGE);
671683
if (owner == null) {
672684
throw new IllegalStateException("Cloud could not be found");
673685
}
686+
Jenkins j = Jenkins.get();
687+
owner.checkManagePermission();
674688
owner.removeTemplate(this);
675689
j.save();
676690
// take the user back.
@@ -680,11 +694,11 @@ public HttpResponse doDoDelete(@AncestorInPath PodTemplateGroup owner) throws IO
680694
@POST
681695
public HttpResponse doConfigSubmit(StaplerRequest2 req, @AncestorInPath PodTemplateGroup owner)
682696
throws IOException, ServletException, Descriptor.FormException {
683-
Jenkins j = Jenkins.get();
684-
j.checkPermission(Jenkins.MANAGE);
685697
if (owner == null) {
686698
throw new IllegalStateException("Cloud could not be found");
687699
}
700+
Jenkins j = Jenkins.get();
701+
owner.checkManagePermission();
688702
PodTemplate newTemplate = reconfigure(req, req.getSubmittedForm());
689703
owner.replaceTemplate(this, newTemplate);
690704
j.save();

src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateGroup.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
package org.csanchez.jenkins.plugins.kubernetes;
2+
3+
import hudson.security.Permission;
4+
import jenkins.model.Jenkins;
5+
import org.springframework.security.access.AccessDeniedException;
6+
import org.springframework.security.core.Authentication;
7+
28
/**
39
* A group of pod templates that can be saved together.
410
*/
@@ -25,4 +31,23 @@ public interface PodTemplateGroup {
2531
* @return the URL to redirect to after the template is saved.
2632
*/
2733
String getPodTemplateGroupUrl();
34+
35+
/**
36+
* @return The permission required to manage the templates in this group.
37+
*/
38+
Permission getManagePermission();
39+
40+
/**
41+
* @return {@code true} if the current {@link Authentication} has permissions to add / replace / remove templates.
42+
*/
43+
default boolean hasManagePermission() {
44+
return Jenkins.get().hasPermission(getManagePermission());
45+
}
46+
/**
47+
* Checks whether the current {@link Authentication} has sufficient permissions to manage the templates in this group.
48+
* @throws AccessDeniedException if access is denied for the current {@link Authentication}.
49+
*/
50+
default void checkManagePermission() throws AccessDeniedException {
51+
Jenkins.get().checkPermission(getManagePermission());
52+
}
2853
}

src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/new.jelly

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ THE SOFTWARE.
2020
<?jelly escape-by-default='true'?>
2121
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
2222
<l:layout permission="${app.MANAGE_AND_SYSTEM_READ}" title="${%New pod template}">
23-
<j:set var="readOnlyMode" value="${!app.hasPermission(app.MANAGE)}"/>
23+
<j:set var="canManageTemplate" value="${it.hasManagePermission()}"/>
24+
<j:set var="readOnlyMode" value="${!canManageTemplate}"/>
2425
<l:breadcrumb title="${%New pod template }"/>
2526
<st:include page="sidepanel.jelly" it="${it}"/>
2627
<l:main-panel>
@@ -30,15 +31,16 @@ THE SOFTWARE.
3031

3132
<j:set var="descriptor" value="${it.templateDescriptor}"/>
3233
<st:include class="${descriptor.clazz}" page="config.jelly"/>
33-
<l:hasAdministerOrManage>
34+
35+
<j:if test="${canManageTemplate}">
3436
<f:bottomButtonBar>
3537
<f:submit value="${%Create}"/>
3638
</f:bottomButtonBar>
37-
</l:hasAdministerOrManage>
39+
</j:if>
3840
</f:form>
39-
<l:hasAdministerOrManage>
41+
<j:if test="${canManageTemplate}">
4042
<st:adjunct includes="lib.form.confirm"/>
41-
</l:hasAdministerOrManage>
43+
</j:if>
4244
</l:main-panel>
4345
</l:layout>
4446
</j:jelly>

src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/templates.jelly

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ THE SOFTWARE.
2929
<j:choose>
3030
<j:when test="${not empty it.templates}">
3131
<l:app-bar title="${it.name} - ${%Pod templates}">
32-
<l:hasAdministerOrManage>
32+
<j:if test="${it.hasManagePermission()}">
3333
<a name="newTemplate" class="jenkins-button jenkins-button--primary" href="new">
3434
<l:icon src="symbol-add"/>
3535
${%Add a pod template}
3636
</a>
37-
</l:hasAdministerOrManage>
37+
</j:if>
3838
</l:app-bar>
3939
<table id="templates" class="jenkins-table sortable">
4040
<thead>
@@ -63,19 +63,21 @@ THE SOFTWARE.
6363
</j:when>
6464
<j:otherwise>
6565
<l:app-bar title="${it.name} - ${%Pod templates}"/>
66-
<div >
67-
<section>
68-
<div>
69-
<div class="jenkins-!-padding-bottom-3">No pod template added yet.</div>
66+
<j:if test="${it.hasManagePermission()}">
67+
<div >
68+
<section>
7069
<div>
71-
<a name="newTemplate" class="jenkins-button jenkins-button--primary" href="new">
72-
<l:icon src="symbol-add"/>
73-
${%Add a pod template}
74-
</a>
70+
<div class="jenkins-!-padding-bottom-3">No pod template added yet.</div>
71+
<div>
72+
<a name="newTemplate" class="jenkins-button jenkins-button--primary" href="new">
73+
<l:icon src="symbol-add"/>
74+
${%Add a pod template}
75+
</a>
76+
</div>
7577
</div>
76-
</div>
77-
</section>
78-
</div>
78+
</section>
79+
</div>
80+
</j:if>
7981
</j:otherwise>
8082
</j:choose>
8183
</l:main-panel>

src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/index.jelly

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ THE SOFTWARE.
2020
<?jelly escape-by-default='true'?>
2121
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
2222
<l:layout permission="${app.MANAGE_AND_SYSTEM_READ}" title="${%Pod template settings}">
23-
<j:set var="readOnlyMode" value="${!app.hasPermission(app.MANAGE)}"/>
23+
<j:set var="canManageTemplate" value="${it.hasManagePermission()}"/>
24+
<j:set var="readOnlyMode" value="${!canManageTemplate}"/>
2425
<l:breadcrumb title="${it.name}"/>
2526

2627
<st:include page="sidepanel.jelly"/>
@@ -37,17 +38,18 @@ THE SOFTWARE.
3738
<!-- main body of the configuration -->
3839
<st:include it="${instance}" page="config.jelly"/>
3940

40-
<l:hasAdministerOrManage>
41+
<j:if test="${canManageTemplate}">
4142
<j:if test="${!instance.readonlyFromUi}">
4243
<f:bottomButtonBar>
4344
<f:submit value="${%Save}"/>
4445
</f:bottomButtonBar>
4546
</j:if>
46-
</l:hasAdministerOrManage>
47+
</j:if>
4748
</f:form>
48-
<l:hasAdministerOrManage>
49+
50+
<j:if test="${canManageTemplate}">
4951
<st:adjunct includes="lib.form.confirm"/>
50-
</l:hasAdministerOrManage>
52+
</j:if>
5153
</l:main-panel>
5254
</l:layout>
5355
</j:jelly>

src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/sidepanel.jelly

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ THE SOFTWARE.
2222
<l:header />
2323
<l:side-panel>
2424
<l:tasks>
25+
<j:set var="canManageTemplate" value="${it.hasManagePermission()}"/>
2526
<l:task href="" icon="symbol-settings"
26-
title="${app.hasPermission(app.MANAGE) ? '%Configure' : '%View Configuration'}"/>
27+
title="${canManageTemplate ? '%Configure' : '%View Configuration'}"/>
2728
<j:if test="${!it.readonlyFromUi}">
28-
<l:delete permission="${app.MANAGE}" title="${%Delete Pod Template}" message="${%delete.template(it.name)}"/>
29+
<l:delete permission="${it.managePermission}" title="${%Delete Pod Template}" message="${%delete.template(it.name)}"/>
2930
</j:if>
3031
<t:actions />
3132
</l:tasks>

src/test/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloudTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
package org.csanchez.jenkins.plugins.kubernetes;
22

3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsString;
35
import static org.junit.Assert.assertEquals;
46
import static org.junit.Assert.assertNull;
7+
import static org.junit.Assert.assertThrows;
58
import static org.junit.Assert.assertTrue;
69
import static org.junit.Assert.fail;
710

11+
import edu.umd.cs.findbugs.annotations.NonNull;
12+
import hudson.model.User;
13+
import hudson.security.ACL;
14+
import hudson.security.ACLContext;
15+
import hudson.security.AccessDeniedException3;
816
import java.util.ArrayList;
917
import java.util.Arrays;
1018
import java.util.Collections;
@@ -14,6 +22,7 @@
1422
import java.util.Set;
1523
import java.util.logging.Level;
1624
import java.util.logging.Logger;
25+
import jenkins.model.Jenkins;
1726
import jenkins.model.JenkinsLocationConfiguration;
1827
import org.apache.commons.beanutils.PropertyUtils;
1928
import org.apache.commons.lang3.RandomStringUtils;
@@ -33,8 +42,10 @@
3342
import org.junit.After;
3443
import org.junit.Rule;
3544
import org.junit.Test;
45+
import org.junit.function.ThrowingRunnable;
3646
import org.jvnet.hudson.test.JenkinsRule;
3747
import org.jvnet.hudson.test.LoggerRule;
48+
import org.jvnet.hudson.test.MockAuthorizationStrategy;
3849
import org.jvnet.hudson.test.recipes.LocalData;
3950

4051
public class KubernetesCloudTest {
@@ -309,4 +320,41 @@ public HtmlInput getInputByName(DomElement root, String name) {
309320
}
310321
return null;
311322
}
323+
324+
@Test
325+
public void authorization() throws Exception {
326+
var securityRealm = j.createDummySecurityRealm();
327+
j.jenkins.setSecurityRealm(securityRealm);
328+
var authorizationStrategy = new MockAuthorizationStrategy();
329+
authorizationStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin");
330+
authorizationStrategy.grant(Jenkins.MANAGE).everywhere().to("manager");
331+
authorizationStrategy.grant(Jenkins.READ).everywhere().to("user");
332+
j.jenkins.setAuthorizationStrategy(authorizationStrategy);
333+
j.jenkins.clouds.add(new KubernetesCloud("kubernetes"));
334+
var pt1 = new PodTemplate("one");
335+
var pt2 = new PodTemplate("two");
336+
try (var ignored = asUser("admin")) {
337+
j.jenkins.clouds.get(KubernetesCloud.class).addTemplate(pt1);
338+
}
339+
try (var ignored = asUser("user")) {
340+
var expectedMessage = "user is missing the Overall/Administer permission";
341+
var kubernetesCloud = j.jenkins.clouds.get(KubernetesCloud.class);
342+
assertAccessDenied(() -> kubernetesCloud.addTemplate(new PodTemplate()), expectedMessage);
343+
assertAccessDenied(() -> kubernetesCloud.removeTemplate(pt1), expectedMessage);
344+
assertAccessDenied(() -> kubernetesCloud.replaceTemplate(pt1, pt2), expectedMessage);
345+
}
346+
try (var ignored = asUser("manager")) {
347+
j.jenkins.clouds.get(KubernetesCloud.class).addTemplate(pt1);
348+
}
349+
}
350+
351+
private static void assertAccessDenied(ThrowingRunnable throwingRunnable, String expectedMessage) {
352+
assertThat(
353+
assertThrows(AccessDeniedException3.class, throwingRunnable).getMessage(),
354+
containsString(expectedMessage));
355+
}
356+
357+
private static @NonNull ACLContext asUser(String admin) {
358+
return ACL.as2(User.get(admin, true, Map.of()).impersonate2());
359+
}
312360
}

0 commit comments

Comments
 (0)