未验证 提交 634004a6 编写于 作者: T Tim Jacomb 提交者: GitHub

JENKINS-61206 System read / Extended read for agents (#4531)

* JENKINS-61206 System read / Extended read for agents

* Update core/src/main/java/hudson/Functions.java

* Set permission to false explicitly

* Add message when no configured clouds

Otherwise there was a white page and it looked weird

* Hide password for Computer.EXTENDED_READ

* Add test

* Access controlled simplification

* Fix typoes  / simplify code

* Simplify task.jelly

* Allow filtering system info extensions

* Show link that should be present

* Adjust MasterComputer access

* Adjust javadoc

* Adjust MasterComputer#configure

* Change links when read only access

* Use it instead of app

* Update core/src/main/resources/hudson/model/Computer/configure.jelly
Co-authored-by: NDaniel Beck <1831569+daniel-beck@users.noreply.github.com>

* Code simplification

* Inline jelly text customisation

* Allow system read to see cloud move blurb

* Tooltip based on permission
Co-authored-by: NDaniel Beck <1831569+daniel-beck@users.noreply.github.com>
上级 4c16f2cd
......@@ -25,6 +25,7 @@
*/
package hudson;
import hudson.model.Computer;
import hudson.model.Slave;
import hudson.security.*;
......@@ -1157,6 +1158,43 @@ public class Functions {
Jenkins.get().hasPermission(d.getRequiredGlobalConfigPagePermission()) || Jenkins.get().hasPermission(Jenkins.SYSTEM_READ)));
}
/**
* Checks if the current security principal has one of the supplied permissions.
*
* @since TODO
*/
public static boolean hasAnyPermission(AccessControlled ac, Permission[] permissions) {
if (permissions == null || permissions.length == 0) {
return true;
}
return ac.hasAnyPermission(permissions);
}
/**
* This version is so that the 'hasAnyPermission'
* degrades gracefully if "it" is not an {@link AccessControlled} object.
* Otherwise it will perform no check and that problem is hard to notice.
*
* @since TODO
*/
public static boolean hasAnyPermission(Object object, Permission[] permissions) throws IOException, ServletException {
if (permissions == null || permissions.length == 0) {
return true;
}
if (object instanceof AccessControlled)
return hasAnyPermission((AccessControlled) object, permissions);
else {
AccessControlled ac = Stapler.getCurrentRequest().findAncestorObject(AccessControlled.class);
if (ac != null) {
return hasAnyPermission(ac, permissions);
}
return hasAnyPermission(Jenkins.get(), permissions);
}
}
/**
* Checks if the current security principal has one of the supplied permissions.
*
......@@ -2034,6 +2072,10 @@ public class Functions {
if (item != null && !item.hasPermission(Item.CONFIGURE)) {
return "********";
}
Computer computer = req.findAncestorObject(Computer.class);
if (computer != null && !computer.hasPermission(Computer.CONFIGURE)) {
return "********";
}
}
}
......
......@@ -333,7 +333,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
* Used to URL-bind {@link AnnotatedLargeText}.
*/
public AnnotatedLargeText<Computer> getLogText() {
checkPermission(CONNECT);
checkAnyPermission(CONNECT, EXTENDED_READ);
return new AnnotatedLargeText<>(getLogFile(), Charset.defaultCharset(), false, this);
}
......@@ -1808,6 +1808,10 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
public static final Permission CONNECT = new Permission(PERMISSIONS,"Connect", Messages._Computer_ConnectPermission_Description(), DISCONNECT, PermissionScope.COMPUTER);
public static final Permission BUILD = new Permission(PERMISSIONS, "Build", Messages._Computer_BuildPermission_Description(), Permission.WRITE, PermissionScope.COMPUTER);
@Restricted(NoExternalUse.class) // called by jelly
public static final Permission[] EXTENDED_READ_AND_CONNECT =
new Permission[] { EXTENDED_READ, CONNECT };
// This permission was historically scoped to this class albeit declared in Cloud. While deserializing, Jenkins loads
// the scope class to make sure the permission is initialized and registered. since Cloud class is used rather seldom,
// it might appear the permission does not exist. Referencing the permission from here to make sure it gets loaded.
......
......@@ -55,7 +55,7 @@ public class NodesLink extends ManagementLink {
@NonNull
@Override
public Permission getRequiredPermission() {
return Jenkins.MANAGE;
return Jenkins.READ;
}
@Override
......
package jenkins.slaves.systemInfo;
import hudson.Extension;
import hudson.model.Computer;
import hudson.security.Permission;
import org.jenkinsci.Symbol;
/**
......@@ -12,4 +14,9 @@ public class ClassLoaderStatisticsSlaveInfo extends SlaveSystemInfo {
public String getDisplayName() {
return Messages.ClassLoaderStatisticsSlaveInfo_DisplayName();
}
@Override
public Permission getRequiredPermission() {
return Computer.EXTENDED_READ;
}
}
package jenkins.slaves.systemInfo;
import hudson.Extension;
import hudson.model.Computer;
import hudson.security.Permission;
import org.jenkinsci.Symbol;
/**
......@@ -12,4 +14,9 @@ public class EnvVarsSlaveInfo extends SlaveSystemInfo {
public String getDisplayName() {
return Messages.EnvVarsSlaveInfo_DisplayName();
}
@Override
public Permission getRequiredPermission() {
return Computer.EXTENDED_READ;
}
}
......@@ -3,6 +3,9 @@ package jenkins.slaves.systemInfo;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Computer;
import hudson.model.ManageJenkinsAction;
import hudson.security.Permission;
import jenkins.model.Jenkins;
/**
* Extension point that contributes to the system information page of {@link Computer}.
......@@ -24,4 +27,15 @@ public abstract class SlaveSystemInfo implements ExtensionPoint {
public static ExtensionList<SlaveSystemInfo> all() {
return ExtensionList.lookup(SlaveSystemInfo.class);
}
/**
* Returns the permission required for user to see this system info extension on the "System Information" page for the Agent
*
* By default {@link Computer#CONNECT}, but {@link Computer#EXTENDED_READ} is also supported.
*
* @return the permission required for the extension to be shown on "System Information".
*/
public Permission getRequiredPermission() {
return Computer.CONNECT;
}
}
package jenkins.slaves.systemInfo;
import hudson.Extension;
import hudson.model.Computer;
import hudson.security.Permission;
import org.jenkinsci.Symbol;
/**
......@@ -12,4 +14,9 @@ public class SystemPropertySlaveInfo extends SlaveSystemInfo {
public String getDisplayName() {
return Messages.SystemPropertySlaveInfo_DisplayName();
}
@Override
public Permission getRequiredPermission() {
return Computer.EXTENDED_READ;
}
}
......@@ -28,7 +28,8 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout permission="${it.CONFIGURE}" title="${%title(it.displayName)}">
<l:layout permission="${it.EXTENDED_READ}" title="${%title(it.displayName)}">
<j:set var="readOnlyMode" value="${!it.hasPermission(it.CONFIGURE)}" />
<st:include page="sidepanel.jelly"/>
<l:main-panel>
<f:form method="post" action="configSubmit" name="config">
......@@ -42,11 +43,15 @@ THE SOFTWARE.
<!-- main body of the configuration -->
<st:include it="${instance}" page="configure-entries.jelly" />
<f:bottomButtonBar>
<f:submit value="${%Save}"/>
</f:bottomButtonBar>
<j:if test="${h.hasPermission(it, it.CONFIGURE)}">
<f:bottomButtonBar>
<f:submit value="${%Save}"/>
</f:bottomButtonBar>
</j:if>
</f:form>
<st:adjunct includes="lib.form.confirm" />
<j:if test="${h.hasPermission(it, it.CONFIGURE)}">
<st:adjunct includes="lib.form.confirm" />
</j:if>
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -33,7 +33,8 @@ THE SOFTWARE.
<l:task contextMenu="false" href="${rootURL}/computer" icon="icon-up icon-md" title="${%Back to List}"/>
<l:task contextMenu="false" href="${rootURL}/${it.url}" icon="icon-search icon-md" title="${%Status}"/>
<l:task href="${rootURL}/${it.url}delete" icon="icon-edit-delete icon-md" permission="${it.DELETE}" title="${%Delete Agent}"/>
<l:task href="${rootURL}/${it.url}configure" icon="icon-setting icon-md" permission="${it.CONFIGURE}" title="${%Configure}"/>
<l:task href="${rootURL}/${it.url}configure" icon="icon-setting icon-md" permission="${it.EXTENDED_READ}"
title="${it.hasPermission(it.CONFIGURE) ? '%Configure' : '%View Configuration'}"/>
<l:task href="${rootURL}/${it.url}builds" icon="icon-notepad icon-md" title="${%Build History}"/>
<l:task href="${rootURL}/${it.url}load-statistics" icon="icon-monitor icon-md" title="${%Load Statistics}"/>
<j:if test="${it.channel!=null}">
......
......@@ -27,7 +27,8 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout permission="${app.ADMINISTER}" title="${%Node Monitoring Configuration}">
<l:layout permission="${app.SYSTEM_READ}" title="${%Node Monitoring Configuration}">
<j:set var="readOnlyMode" value="${!app.hasPermission(app.ADMINISTER)}" />
<st:include page="sidepanel.jelly" />
<l:main-panel>
<!-- to make the form field binding work -->
......@@ -39,10 +40,12 @@ THE SOFTWARE.
descriptors="${it.nodeMonitorDescriptors}"
instances="${it.nonIgnoredMonitors}" />
<f:bottomButtonBar>
<f:submit value="${%OK}" />
<f:apply />
</f:bottomButtonBar>
<l:isAdmin>
<f:bottomButtonBar>
<f:submit value="${%OK}" />
<f:apply />
</f:bottomButtonBar>
</l:isAdmin>
</f:form>
<st:adjunct includes="lib.form.confirm" />
</l:main-panel>
......
......@@ -66,9 +66,9 @@ THE SOFTWARE.
</j:forEach>
<td><!-- config link -->
<j:if test="${c.hasPermission(c.CONFIGURE)}">
<j:if test="${c.hasPermission(c.EXTENDED_READ)}">
<a href="${rootURL}/${c.url}configure">
<l:icon class="icon-gear2 icon-lg" tooltip="${%Configure}"/>
<l:icon class="icon-gear2 icon-lg" tooltip="${c.hasPermission(c.CONFIGURE) ? '%Configure' : '%View Configuration'}"/>
</a>
</j:if>
</td>
......
......@@ -32,12 +32,13 @@ THE SOFTWARE.
<j:getStatic var="createPermission" className="hudson.model.Computer" field="CREATE"/>
<l:tasks>
<l:task href="${rootURL}/" icon="icon-up icon-md" title="${%Back to Dashboard}"/>
<l:task href="${rootURL}/manage" icon="icon-gear2 icon-md" permission="${app.MANAGE}" title="${%Manage Jenkins}"/>
<l:task href="${rootURL}/manage" icon="icon-gear2 icon-md" permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%Manage Jenkins}"/>
<l:task href="new" icon="icon-new-computer icon-md" permission="${createPermission}" title="${%New Node}"/>
<l:task href="${rootURL}/configureClouds" icon="icon-health-40to59 icon-md" permission="${app.ADMINISTER}" title="${%Configure Clouds}"/>
<l:task href="configure" icon="icon-gear2 icon-md" permission="${app.ADMINISTER}" title="${%Node Monitoring}"/>
<l:task href="${rootURL}/configureClouds" icon="icon-health-40to59 icon-md" permission="${app.SYSTEM_READ}"
title="${app.hasPermission(app.ADMINISTER) ? '%Configure Clouds' : '%View Clouds'}"/>
<l:task href="configure" icon="icon-gear2 icon-md" permission="${app.SYSTEM_READ}" title="${%Node Monitoring}"/>
</l:tasks>
<t:queue items="${app.queue.items}" />
<t:executors />
</l:side-panel>
</j:jelly>
\ No newline at end of file
</j:jelly>
......@@ -24,23 +24,21 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="${it.displayName} log" secured="true">
<l:layout title="${it.displayName} log" permissions="${it.EXTENDED_READ_AND_CONNECT}">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<l:hasPermission permission="${it.CONNECT}">
<pre id="out" />
<div id="spinner">
<img src="${imagesURL}/spinner.gif" alt=""/>
</div>
<t:progressiveText href="logText/progressiveHtml" idref="out" spinner="spinner" />
<!-- TODO dubious value: INFO+ shown in logText anyway; FINE- configured in /log/*/ maybe better viewed there
<j:set var="logRecords" value="${it.logRecords}"/>
<j:if test="${!logRecords.isEmpty()}">
<h1>${%Log Records}</h1>
<t:logRecords logRecords="${logRecords}"/>
</j:if>
-->
</l:hasPermission>
<pre id="out" />
<div id="spinner">
<img src="${imagesURL}/spinner.gif" alt=""/>
</div>
<t:progressiveText href="logText/progressiveHtml" idref="out" spinner="spinner" />
<!-- TODO dubious value: INFO+ shown in logText anyway; FINE- configured in /log/*/ maybe better viewed there
<j:set var="logRecords" value="${it.logRecords}"/>
<j:if test="${!logRecords.isEmpty()}">
<h1>${%Log Records}</h1>
<t:logRecords logRecords="${logRecords}"/>
</j:if>
-->
</l:main-panel>
</l:layout>
</j:jelly>
\ No newline at end of file
......@@ -24,9 +24,9 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:task icon="icon-clipboard icon-md" href="${rootURL}/${it.url}log" title="${%Log}" permission="${it.CONNECT}" />
<l:task icon="icon-clipboard icon-md" href="${rootURL}/${it.url}log" title="${%Log}" permissions="${it.EXTENDED_READ_AND_CONNECT}" />
<j:if test="${it.channel!=null}">
<l:task icon="icon-computer icon-md" href="${rootURL}/${it.url}systemInfo" title="${%System Information}" permission="${it.CONNECT}"/>
<l:task icon="icon-computer icon-md" href="${rootURL}/${it.url}systemInfo" title="${%System Information}" permissions="${it.EXTENDED_READ_AND_CONNECT}"/>
<l:task icon="icon-edit-delete icon-md" href="${rootURL}/${it.url}disconnect" title="${%Disconnect}" permission="${it.DISCONNECT}"/>
</j:if>
......
......@@ -29,7 +29,7 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout title="${it.displayName} ${%System Information}">
<l:layout title="${it.displayName} ${%System Information}" permissions="${it.EXTENDED_READ_AND_CONNECT}">
<st:include page="sidepanel.jelly" />
<l:main-panel>
......@@ -42,21 +42,21 @@ THE SOFTWARE.
</j:if>
</h1>
<l:hasPermission permission="${it.CONNECT}">
<j:choose>
<j:when test="${it.channel != null}">
<h2>${it.oSDescription} agent, version ${it.slaveVersion}</h2>
<j:choose>
<j:when test="${it.channel != null}">
<h2>${it.oSDescription} agent, version ${it.slaveVersion}</h2>
<j:forEach var="instance" items="${it.systemInfoExtensions}">
<j:forEach var="instance" items="${it.systemInfoExtensions}">
<l:hasPermission permission="${instance.requiredPermission}">
<h1>${instance.displayName}</h1>
<st:include page="systemInfo" from="${instance}"/>
</j:forEach>
</j:when>
<j:otherwise>
${%System Information is unavailable when agent is offline.}
</j:otherwise>
</j:choose>
</l:hasPermission>
</l:hasPermission>
</j:forEach>
</j:when>
<j:otherwise>
${%System Information is unavailable when agent is offline.}
</j:otherwise>
</j:choose>
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -8,7 +8,8 @@ def f = namespace(lib.FormTagLib)
def l = namespace(lib.LayoutTagLib)
def st = namespace("jelly:stapler")
l.layout(norefresh:true, permission:app.ADMINISTER, title:my.displayName) {
l.layout(norefresh:true, permission:app.SYSTEM_READ, title:my.displayName) {
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
l.side_panel {
l.tasks {
l.task(icon:"icon-up icon-md", href:rootURL+'/', title:_("Back to Dashboard"))
......@@ -21,25 +22,31 @@ l.layout(norefresh:true, permission:app.ADMINISTER, title:my.displayName) {
// TODO more appropriate icon
text(my.displayName)
}
def clouds = Cloud.all()
if (!clouds.isEmpty()) {
p()
div(class:"behavior-loading", _("LOADING"))
f.form(method:"post",name:"config",action:"configure") {
f.block {
if (app.clouds.size() == 0 && !h.hasPermission(app.ADMINISTER)) {
p(_("No clouds have been configured."))
}
f.hetero_list(name:"cloud", hasHeader:true, descriptors:Cloud.all(), items:app.clouds,
addCaption:_("Add a new cloud"), deleteCaption:_("Delete cloud"))
}
f.bottomButtonBar {
f.submit(value:_("Save"))
f.apply(value:_("Apply"))
l.isAdmin {
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply(value: _("Apply"))
}
}
}
st.adjunct(includes: "lib.form.confirm")
l.isAdmin {
st.adjunct(includes: "lib.form.confirm")
}
} else {
String label = Jenkins.get().updateCenter.getCategoryDisplayName("cloud")
......
......@@ -28,7 +28,8 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout permission="${it.CONFIGURE}" title="${it.displayName}">
<l:layout permission="${it.EXTENDED_READ}" title="${it.displayName}">
<j:set var="readOnlyMode" value="${!h.hasPermission(it, it.CONFIGURE)}" />
<st:include page="sidepanel.jelly"/>
<l:main-panel>
<f:form method="post" action="configSubmit" name="config">
......@@ -53,9 +54,11 @@ THE SOFTWARE.
<f:descriptorList title="${%Node Properties}" descriptors="${h.getNodePropertyDescriptors(descriptor.clazz)}" field="nodeProperties" />
<f:block>
<f:submit value="${%Save}"/>
</f:block>
<l:hasPermission permission="${it.CONFIGURE}">
<f:block>
<f:submit value="${%Save}"/>
</f:block>
</l:hasPermission>
</f:form>
</l:main-panel>
</l:layout>
......
......@@ -54,9 +54,9 @@ THE SOFTWARE.
</f:rowSet>
</j:forEach>
<l:isAdmin>
<l:hasPermission permission="${app.SYSTEM_READ}">
<st:include page="_cloud-note.jelly"/>
</l:isAdmin>
</l:hasPermission>
<l:hasAdministerOrManage>
<f:bottomButtonBar>
......
......@@ -56,6 +56,14 @@ THE SOFTWARE.
This is useful for showing links to restricted pages, as showing
them to unprivileged users don't make sense.
If both permission and permissions is set, then permissions will be used
</st:attribute>
<st:attribute name="permissions">
If specified, the link will be only displayed when the current user has
one or more of the specified permissions against the "it" object.
If both permission and permissions is set, then permissions will be used
</st:attribute>
<st:attribute name="post" type="boolean">
If true, send a POST rather than a GET request.
......@@ -84,8 +92,19 @@ THE SOFTWARE.
<j:set var="parentTagContext" value="${context.parent}"/>
<d:invokeBody />
</j:scope>
<j:if test="${attrs.permission != null}">
<j:set var="hasPermission" value="${h.hasPermission(it, attrs.permission)}" />
</j:if>
<j:if test="${attrs.permissions != null}">
<j:set var="hasPermission" value="${h.hasAnyPermission(it, attrs.permissions)}" />
</j:if>
<j:if test="${attrs.permission == null and attrs.permissions == null}">
<j:set var="hasPermission" value="true" />
</j:if>
<j:if test="${attrs.permission==null or h.hasPermission(it,attrs.permission)}">
<j:if test="${hasPermission}">
<j:choose>
<j:when test="${parentTagContext!=null}">
<j:if test="${isCurrent or hasMatchingChild}">
......
......@@ -682,4 +682,63 @@ public class PasswordTest {
return "stringPassword";
}
}
@Test
public void computerExtendedReadNoSecretsRevealed() throws Exception {
Computer computer = j.jenkins.getComputers()[0];
computer.addAction(new SecuredAction());
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
final String ADMIN = "admin";
final String READONLY = "readonly";
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
// full access
.grant(Jenkins.ADMINISTER).everywhere().to(ADMIN)
// Extended access
.grant(Computer.EXTENDED_READ).everywhere().to(READONLY)
.grant(Jenkins.READ).everywhere().to(READONLY)
);
JenkinsRule.WebClient wc = j.createWebClient();
{
wc.login(READONLY);
HtmlPage page = wc.goTo("computer/(master)/secured/");
String value = ((HtmlInput)page.getElementById("password")).getValueAttribute();
assertThat(value, is("********"));
}
{
wc.login(ADMIN);
HtmlPage page = wc.goTo("computer/(master)/secured/");
String value = ((HtmlInput)page.getElementById("password")).getValueAttribute();
assertThat(Secret.fromString(value).getPlainText(), is("abcdefgh"));
}
}
public static class SecuredAction implements Action {
public final Secret secret = Secret.fromString("abcdefgh");
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return "Secured";
}
@Override
public String getUrlName() {
return "secured";
}
}
}
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<l:layout title="Making sure the password field gets ******** for Computer/ExtendedRead">
<l:main-panel>
<f:form method="post" name="config" action="thisFormWillNotBeSubmitted">
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${it.descriptor}" />
<f:entry field="secret">
<f:password id="password" />
</f:entry>
</f:form>
</l:main-panel>
</l:layout>
</j:jelly>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册