Skip to content

Commit 3aea04c

Browse files
C​⁠‌​⁠⁠‌​​⁠‍‌‌​​‍‌yprien Q​⁠‌​⁠⁠‌​​⁠‍‌‌​​‍‌uilicitimja
andauthored
Allow reloading configuration with Overall/Manage permission (jenkinsci#1653)
Co-authored-by: Tim Jacomb <timjacomb1+github@gmail.com>
1 parent 3ebcd64 commit 3aea04c

6 files changed

Lines changed: 213 additions & 23 deletions

File tree

.idea/codeStyles/Project.xml

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public String getDescription() {
151151
@NonNull
152152
@Override
153153
public Permission getRequiredPermission() {
154-
return Jenkins.SYSTEM_READ;
154+
return Jenkins.READ;
155155
}
156156

157157
public Date getLastTimeLoaded() {
@@ -165,7 +165,7 @@ public List<String> getSources() {
165165
@RequirePOST
166166
@Restricted(NoExternalUse.class)
167167
public void doReload(StaplerRequest request, StaplerResponse response) throws Exception {
168-
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
168+
if (!Jenkins.get().hasPermission(Jenkins.MANAGE)) {
169169
response.sendError(HttpServletResponse.SC_FORBIDDEN);
170170
return;
171171
}

plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/index.jelly

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?jelly escape-by-default='true'?>
22
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:st="jelly:stapler">
3-
<l:layout norefresh="true" type="one-column" title="${%Configuration as Code}">
3+
<l:layout norefresh="true" type="one-column" title="${%Configuration as Code}" permissions="${app.MANAGE_AND_SYSTEM_READ}">
44
<l:main-panel>
55
<st:adjunct includes="io.jenkins.plugins.casc.assets.index"/>
66

@@ -39,27 +39,29 @@
3939
</l:isAdmin>
4040
<h2>${%Actions}</h2>
4141

42-
<l:isAdmin>
42+
<l:hasAdministerOrManage>
4343
<f:form method="post" action="reload" name="reload">
4444
<f:submit name="reload" value="${%Reload existing configuration}"/>
4545
</f:form>
46-
</l:isAdmin>
47-
<f:form method="post" action="export" name="export">
48-
<f:submit name="export" value="${%Download Configuration}"/>
49-
</f:form>
50-
<f:form method="post" action="viewExport" name="viewExport">
51-
<f:submit name="viewExport" value="${%View Configuration}"/>
52-
</f:form>
53-
<div class="alert alert-warning clear-action-forms">
54-
<img src="${rootURL}/images/16x16/warning.png"/>
55-
${%exportWarning}
56-
</div>
46+
</l:hasAdministerOrManage>
47+
<l:hasPermission permission="${app.SYSTEM_READ}">
48+
<f:form method="post" action="export" name="export">
49+
<f:submit name="export" value="${%Download Configuration}"/>
50+
</f:form>
51+
<f:form method="post" action="viewExport" name="viewExport">
52+
<f:submit name="viewExport" value="${%View Configuration}"/>
53+
</f:form>
54+
<div class="alert alert-warning clear-action-forms">
55+
<img src="${rootURL}/images/16x16/warning.png"/>
56+
${%exportWarning}
57+
</div>
5758

58-
<h2>${%Reference}</h2>
59-
<dt>
60-
<dl><a href="reference">${%Documentation}</a></dl>
61-
<dl><a href="schema">${%JSON schema}</a></dl>
62-
</dt>
59+
<h2>${%Reference}</h2>
60+
<dt>
61+
<dl><a href="reference">${%Documentation}</a></dl>
62+
<dl><a href="schema">${%JSON schema}</a></dl>
63+
</dt>
64+
</l:hasPermission>
6365

6466
</div>
6567
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.jenkins.plugins.casc.permissions;
2+
3+
public enum Action {
4+
5+
VIEW_CONFIGURATION("View Configuration"),
6+
DOWNLOAD_CONFIGURATION("Download Configuration"),
7+
APPLY_NEW_CONFIGURATION("Apply new configuration"),
8+
RELOAD_EXISTING_CONFIGURATION("Reload existing configuration"),
9+
;
10+
11+
String buttonText;
12+
13+
Action(String buttonText) {
14+
this.buttonText = buttonText;
15+
}
16+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package io.jenkins.plugins.casc.permissions;
2+
3+
import com.gargoylesoftware.htmlunit.html.HtmlPage;
4+
import com.google.common.collect.ImmutableMap;
5+
import java.util.Map;
6+
import jenkins.model.Jenkins;
7+
import org.junit.Rule;
8+
import org.junit.Test;
9+
import org.jvnet.hudson.test.JenkinsRule;
10+
import org.jvnet.hudson.test.JenkinsRule.WebClient;
11+
import org.jvnet.hudson.test.MockAuthorizationStrategy;
12+
13+
import static io.jenkins.plugins.casc.permissions.Action.APPLY_NEW_CONFIGURATION;
14+
import static io.jenkins.plugins.casc.permissions.Action.DOWNLOAD_CONFIGURATION;
15+
import static io.jenkins.plugins.casc.permissions.Action.RELOAD_EXISTING_CONFIGURATION;
16+
import static io.jenkins.plugins.casc.permissions.Action.VIEW_CONFIGURATION;
17+
import static java.lang.String.format;
18+
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
19+
import static java.net.HttpURLConnection.HTTP_OK;
20+
import static org.hamcrest.MatcherAssert.assertThat;
21+
import static org.hamcrest.Matchers.containsString;
22+
import static org.hamcrest.Matchers.is;
23+
import static org.hamcrest.Matchers.not;
24+
import static org.junit.Assert.assertEquals;
25+
26+
public class PermissionsTest {
27+
28+
private static final String RELATIVE_PATH_MANAGE_PAGE = "manage";
29+
private static final String RELATIVE_PATH_CASC_PAGE = "configuration-as-code";
30+
31+
@Rule
32+
public JenkinsRule j = new JenkinsRule();
33+
34+
@Test
35+
public void checkPermissionsForReader() throws Exception {
36+
final String READER = "reader";
37+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
38+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
39+
.grant(Jenkins.READ).everywhere().to(READER)
40+
);
41+
42+
JenkinsRule.WebClient webClient = j.createWebClient()
43+
.withThrowExceptionOnFailingStatusCode(false);
44+
45+
webClient.login(READER);
46+
assertCannotAccessPage(webClient, RELATIVE_PATH_CASC_PAGE);
47+
assertCannotAccessPage(webClient, RELATIVE_PATH_MANAGE_PAGE);
48+
}
49+
50+
@Test
51+
public void checkPermissionsForSystemReader() throws Exception {
52+
final String SYSTEM_READER = "systemReader";
53+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
54+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
55+
.grant(Jenkins.READ).everywhere().to(SYSTEM_READER)
56+
.grant(Jenkins.SYSTEM_READ).everywhere().to(SYSTEM_READER)
57+
);
58+
59+
JenkinsRule.WebClient webClient = j.createWebClient()
60+
.withThrowExceptionOnFailingStatusCode(false);
61+
62+
assertUserPermissions(
63+
webClient,
64+
SYSTEM_READER,
65+
ImmutableMap.<Action, Boolean>builder()
66+
.put(VIEW_CONFIGURATION, true)
67+
.put(DOWNLOAD_CONFIGURATION, true)
68+
.put(APPLY_NEW_CONFIGURATION, false)
69+
.put(RELOAD_EXISTING_CONFIGURATION, false)
70+
.build()
71+
);
72+
}
73+
74+
@Test
75+
public void checkPermissionsForManager() throws Exception {
76+
final String MANAGER = "manager";
77+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
78+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
79+
.grant(Jenkins.READ).everywhere().to(MANAGER)
80+
.grant(Jenkins.MANAGE).everywhere().to(MANAGER)
81+
);
82+
83+
JenkinsRule.WebClient webClient = j.createWebClient()
84+
.withThrowExceptionOnFailingStatusCode(false);
85+
86+
assertUserPermissions(
87+
webClient,
88+
MANAGER,
89+
ImmutableMap.<Action, Boolean>builder()
90+
.put(RELOAD_EXISTING_CONFIGURATION, true)
91+
.build()
92+
);
93+
}
94+
95+
@Test
96+
public void checkPermissionsForAdmin() throws Exception {
97+
final String ADMIN = "admin";
98+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
99+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
100+
.grant(Jenkins.ADMINISTER).everywhere().to(ADMIN)
101+
);
102+
103+
JenkinsRule.WebClient webClient = j.createWebClient()
104+
.withThrowExceptionOnFailingStatusCode(false);
105+
106+
assertUserPermissions(
107+
webClient,
108+
ADMIN,
109+
ImmutableMap.<Action, Boolean>builder()
110+
.put(VIEW_CONFIGURATION, true)
111+
.put(DOWNLOAD_CONFIGURATION, true)
112+
.put(APPLY_NEW_CONFIGURATION, true)
113+
.put(RELOAD_EXISTING_CONFIGURATION, true)
114+
.build()
115+
);
116+
}
117+
118+
private void assertUserPermissions(WebClient webClient, String user,
119+
Map<Action, Boolean> allowedActions) throws Exception {
120+
121+
webClient.login(user);
122+
assertCascTileShows(webClient);
123+
124+
HtmlPage cascPage = assertCanAccessPage(webClient, RELATIVE_PATH_CASC_PAGE);
125+
allowedActions.forEach(
126+
(action, isAllowed) -> assertActionAvailable(cascPage, action, isAllowed)
127+
);
128+
}
129+
130+
private HtmlPage assertCanAccessPage(WebClient webClient, String relativePath)
131+
throws Exception {
132+
HtmlPage page = webClient.goTo(relativePath);
133+
assertEquals(HTTP_OK, page.getWebResponse().getStatusCode());
134+
return page;
135+
}
136+
137+
private void assertCannotAccessPage(WebClient webClient, String relativePath) throws Exception {
138+
final HtmlPage page = webClient.goTo(relativePath);
139+
final int statusCode = page.getWebResponse().getStatusCode();
140+
assertThat(
141+
format("Page %s should not be accessible", relativePath),
142+
statusCode,
143+
is(HTTP_FORBIDDEN)
144+
);
145+
}
146+
147+
private void assertCascTileShows(WebClient webClient) throws Exception {
148+
HtmlPage managePage = assertCanAccessPage(webClient, RELATIVE_PATH_MANAGE_PAGE);
149+
final String pageContent = managePage.getWebResponse().getContentAsString();
150+
assertThat(
151+
"The user should have access to the CasC tile in management page",
152+
pageContent,
153+
containsString("Configuration as Code")
154+
);
155+
}
156+
157+
private void assertActionAvailable(HtmlPage page, Action action, boolean shouldContain) {
158+
String responseContent = page.getWebResponse().getContentAsString();
159+
if (shouldContain) {
160+
assertThat(
161+
format("Action %s should be available", action.name()),
162+
responseContent,
163+
containsString(action.buttonText)
164+
);
165+
} else {
166+
assertThat(
167+
format("Action %s should not be available", action.name()),
168+
responseContent,
169+
not(containsString(action.buttonText))
170+
);
171+
}
172+
}
173+
}

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<properties>
2222
<revision>1.52</revision>
2323
<changelist>-SNAPSHOT</changelist>
24-
<jenkins.version>2.222.1</jenkins.version>
24+
<jenkins.version>2.249.1</jenkins.version>
2525
<java.level>8</java.level>
2626
<tagNameFormat>configuration-as-code-@{project.version}</tagNameFormat>
2727
<useBeta>true</useBeta>

0 commit comments

Comments
 (0)