Skip to content

Commit e6e7e36

Browse files
authored
Add support for node properties (#143)
1 parent 34c2c5a commit e6e7e36

File tree

9 files changed

+364
-6
lines changed

9 files changed

+364
-6
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package com.microsoft.jenkins.azuread;
2+
3+
import edu.umd.cs.findbugs.annotations.NonNull;
4+
import hudson.Extension;
5+
import hudson.model.Computer;
6+
import hudson.model.Node;
7+
import hudson.model.User;
8+
import hudson.security.AuthorizationStrategy;
9+
import hudson.security.Permission;
10+
import hudson.security.PermissionScope;
11+
import hudson.slaves.NodePropertyDescriptor;
12+
import hudson.util.FormValidation;
13+
import jenkins.model.Jenkins;
14+
import jenkins.model.NodeListener;
15+
import net.sf.json.JSONObject;
16+
import org.jenkinsci.Symbol;
17+
import org.jenkinsci.plugins.matrixauth.AbstractAuthorizationPropertyConverter;
18+
import org.jenkinsci.plugins.matrixauth.AuthorizationMatrixNodeProperty;
19+
import org.jenkinsci.plugins.matrixauth.AuthorizationPropertyDescriptor;
20+
import org.kohsuke.accmod.Restricted;
21+
import org.kohsuke.accmod.restrictions.DoNotUse;
22+
import org.kohsuke.accmod.restrictions.NoExternalUse;
23+
import org.kohsuke.accmod.restrictions.suppressions.SuppressRestrictedWarnings;
24+
import org.kohsuke.stapler.AncestorInPath;
25+
import org.kohsuke.stapler.QueryParameter;
26+
import org.kohsuke.stapler.StaplerRequest;
27+
28+
import java.io.IOException;
29+
import java.util.Collections;
30+
import java.util.logging.Level;
31+
import java.util.logging.Logger;
32+
33+
public class AzureAdAuthorizationMatrixNodeProperty extends AuthorizationMatrixNodeProperty {
34+
35+
private final transient ObjId2FullSidMap objId2FullSidMap = new ObjId2FullSidMap();
36+
37+
public AzureAdAuthorizationMatrixNodeProperty() {
38+
super(Collections.emptyMap());
39+
}
40+
41+
void refreshMap() {
42+
for (String fullSid : this.getAllSIDs()) {
43+
objId2FullSidMap.putFullSid(fullSid);
44+
}
45+
new AzureAdAuthorizationMatrixNodeProperty();
46+
}
47+
48+
@Override
49+
public void add(Permission p, String sid) {
50+
super.add(p, sid);
51+
objId2FullSidMap.putFullSid(sid);
52+
}
53+
54+
@Override
55+
public boolean hasExplicitPermission(String sid, Permission p) {
56+
// Jenkins will pass in the object Id as sid
57+
final String objectId = sid;
58+
if (objectId == null) {
59+
return false;
60+
}
61+
return super.hasExplicitPermission(objId2FullSidMap.getOrOriginal(objectId), p);
62+
}
63+
64+
@Override
65+
public boolean hasPermission(String sid, Permission p) {
66+
// Jenkins will pass in the object Id as sid
67+
final String objectId = sid;
68+
return super.hasPermission(objId2FullSidMap.getOrOriginal(objectId), p);
69+
}
70+
71+
@Override
72+
public boolean hasPermission(String sid, Permission p, boolean principal) {
73+
// Jenkins will pass in the object Id as sid
74+
final String objectId = sid;
75+
return super.hasPermission(objId2FullSidMap.getOrOriginal(objectId), p, principal);
76+
}
77+
78+
/**
79+
* Persist {@link AzureAdAuthorizationMatrixNodeProperty} as a list of IDs that
80+
* represent {@link AzureAdAuthorizationMatrixNodeProperty#getGrantedPermissions()}.
81+
*/
82+
@Restricted(NoExternalUse.class)
83+
@SuppressRestrictedWarnings(AbstractAuthorizationPropertyConverter.class)
84+
public static final class ConverterImpl extends
85+
AbstractAuthorizationPropertyConverter<AzureAdAuthorizationMatrixNodeProperty> {
86+
public boolean canConvert(Class type) {
87+
return type == AzureAdAuthorizationMatrixNodeProperty.class;
88+
}
89+
90+
public AzureAdAuthorizationMatrixNodeProperty create() {
91+
return new AzureAdAuthorizationMatrixNodeProperty();
92+
}
93+
}
94+
95+
@Extension
96+
@Symbol("azureAdAuthorizationMatrix")
97+
@SuppressRestrictedWarnings(AuthorizationPropertyDescriptor.class)
98+
public static class DescriptorImpl extends NodePropertyDescriptor
99+
implements AuthorizationPropertyDescriptor<AzureAdAuthorizationMatrixNodeProperty> {
100+
101+
@Override
102+
public AzureAdAuthorizationMatrixNodeProperty create() {
103+
return new AzureAdAuthorizationMatrixNodeProperty();
104+
}
105+
106+
@Override
107+
public PermissionScope getPermissionScope() {
108+
return PermissionScope.COMPUTER;
109+
}
110+
111+
@Override
112+
public AzureAdAuthorizationMatrixNodeProperty newInstance(
113+
StaplerRequest req,
114+
@NonNull JSONObject formData
115+
) throws FormException {
116+
return createNewInstance(req, formData, false);
117+
}
118+
119+
@Override
120+
public boolean isApplicable() {
121+
return Jenkins.get().getAuthorizationStrategy() instanceof AzureAdMatrixAuthorizationStrategy;
122+
}
123+
124+
@NonNull
125+
@Override
126+
public String getDisplayName() {
127+
return "Azure Active Directory Authorization Matrix";
128+
}
129+
130+
@SuppressWarnings("unused") // called by jelly
131+
public boolean isDisableGraphIntegration() {
132+
AzureSecurityRealm securityRealm = (AzureSecurityRealm) Jenkins.get().getSecurityRealm();
133+
return securityRealm.isDisableGraphIntegration();
134+
}
135+
136+
@Restricted(DoNotUse.class)
137+
public FormValidation doCheckName(@AncestorInPath Computer computer, @QueryParameter String value) {
138+
if (isDisableGraphIntegration()) {
139+
return Utils.undecidableResponse(value);
140+
}
141+
142+
// Computer isn't a DescriptorByNameOwner before Jenkins 2.78, and then @AncestorInPath doesn't work
143+
return doCheckName_(value,
144+
computer == null ? Jenkins.get() : computer,
145+
computer == null ? Jenkins.ADMINISTER : Computer.CONFIGURE);
146+
}
147+
}
148+
149+
/**
150+
* Ensure that the user creating a node has Read and Configure permissions.
151+
*/
152+
@Extension
153+
@Restricted(NoExternalUse.class)
154+
public static class NodeListenerImpl extends NodeListener {
155+
@Override
156+
protected void onCreated(@NonNull Node node) {
157+
AuthorizationStrategy authorizationStrategy = Jenkins.get().getAuthorizationStrategy();
158+
if (authorizationStrategy instanceof AzureAdMatrixAuthorizationStrategy) {
159+
AzureAdMatrixAuthorizationStrategy strategy =
160+
(AzureAdMatrixAuthorizationStrategy) authorizationStrategy;
161+
162+
AuthorizationMatrixNodeProperty prop = node
163+
.getNodeProperty(AzureAdAuthorizationMatrixNodeProperty.class);
164+
if (prop == null) {
165+
prop = new AzureAdAuthorizationMatrixNodeProperty();
166+
}
167+
168+
User current = User.current();
169+
String sid = current == null ? "anonymous" : current.getId();
170+
171+
if (!strategy.getACL(node).hasPermission2(Jenkins.getAuthentication2(), Computer.CONFIGURE)) {
172+
prop.add(Computer.CONFIGURE, sid);
173+
}
174+
if (!prop.getGrantedPermissions().isEmpty()) {
175+
try {
176+
node.getNodeProperties().replace(prop);
177+
} catch (IOException ex) {
178+
LOGGER.log(Level.WARNING, "Failed to grant creator permissions on node "
179+
+ node.getDisplayName(), ex);
180+
}
181+
}
182+
}
183+
}
184+
}
185+
186+
private static final Logger LOGGER = Logger.getLogger(AzureAdAuthorizationMatrixNodeProperty.class.getName());
187+
}

src/main/java/com/microsoft/jenkins/azuread/AzureAdMatrixAuthorizationStrategy.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import hudson.model.Item;
2525
import hudson.model.ItemGroup;
2626
import hudson.model.Job;
27+
import hudson.model.Node;
2728
import hudson.security.ACL;
2829
import hudson.security.AuthorizationStrategy;
2930
import hudson.security.GlobalMatrixAuthorizationStrategy;
@@ -34,6 +35,7 @@
3435
import okhttp3.Request;
3536
import org.apache.commons.lang.StringUtils;
3637
import org.jenkinsci.plugins.matrixauth.AuthorizationContainer;
38+
import org.jenkinsci.plugins.matrixauth.AuthorizationMatrixNodeProperty;
3739
import org.kohsuke.accmod.Restricted;
3840
import org.kohsuke.accmod.restrictions.DoNotUse;
3941
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -70,6 +72,16 @@ public ACL getACL(@NonNull Job<?, ?> project) {
7072
}
7173
}
7274

75+
@NonNull
76+
@Override
77+
public ACL getACL(@NonNull Node node) {
78+
AuthorizationMatrixNodeProperty property = node.getNodeProperty(AzureAdAuthorizationMatrixNodeProperty.class);
79+
if (property != null) {
80+
return property.getInheritanceStrategy().getEffectiveACL(property.getACL(), node);
81+
}
82+
return getRootACL();
83+
}
84+
7385
@Restricted(NoExternalUse.class)
7486
public static ACL inheritingACL(final ACL parent, final ACL child) {
7587
if (parent instanceof SidACL && child instanceof SidACL) {

src/main/java/com/microsoft/jenkins/azuread/AzureSecurityRealm.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ public HttpResponse doFinishLogin(StaplerRequest request)
358358
try {
359359
final Long beginTime = (Long) request.getSession().getAttribute(TIMESTAMP_ATTRIBUTE);
360360
final String expectedNonce = (String) request.getSession().getAttribute(NONCE_ATTRIBUTE);
361+
if (expectedNonce == null) {
362+
// no nonce, probably some issue with an old session, force the user to re-auth
363+
request.getSession().invalidate();
364+
return HttpResponses.redirectToContextRoot();
365+
}
366+
361367
if (beginTime != null) {
362368
long endTime = System.currentTimeMillis();
363369
LOGGER.info("Requesting oauth code time = " + (endTime - beginTime) + " ms");

src/main/java/com/microsoft/jenkins/azuread/GraphProxy.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import edu.umd.cs.findbugs.annotations.NonNull;
77
import hudson.Extension;
88
import hudson.model.Action;
9+
import hudson.model.Computer;
910
import hudson.model.Job;
1011
import hudson.model.RootAction;
1112
import hudson.model.User;
13+
import hudson.security.AccessControlled;
1214
import hudson.security.SecurityRealm;
1315
import jenkins.model.Jenkins;
1416
import jenkins.model.TransientActionFactory;
@@ -45,7 +47,7 @@ public class GraphProxy implements RootAction, StaplerProxy {
4547
.expireAfterWrite(TEN, TimeUnit.MINUTES)
4648
.build();
4749

48-
private Job<?, ?> job;
50+
private AccessControlled accessControlled;
4951

5052
@Override
5153
public String getIconFileName() {
@@ -67,14 +69,21 @@ public String getUrlName() {
6769
public GraphProxy() {
6870
}
6971

70-
public GraphProxy(Job<?, ?> job) {
71-
this.job = job;
72+
public GraphProxy(AccessControlled accessControlled) {
73+
this.accessControlled = accessControlled;
7274
}
7375

7476
@Override
7577
public Object getTarget() {
76-
if (job != null) {
77-
job.checkPermission(Job.CONFIGURE);
78+
if (accessControlled != null) {
79+
if (accessControlled instanceof Job) {
80+
accessControlled.checkPermission(Job.CONFIGURE);
81+
} else if (accessControlled instanceof Computer) {
82+
accessControlled.checkPermission(Computer.CONFIGURE);
83+
} else {
84+
accessControlled.checkPermission(Jenkins.ADMINISTER);
85+
}
86+
7887
return this;
7988
}
8089

@@ -98,6 +107,21 @@ public Collection<? extends Action> createFor(@NonNull Job target) {
98107
}
99108
}
100109

110+
@Extension
111+
public static class TransientActionFactoryComputer extends TransientActionFactory<Computer> {
112+
113+
@Override
114+
public Class<Computer> type() {
115+
return Computer.class;
116+
}
117+
118+
@NonNull
119+
@Override
120+
public Collection<? extends Action> createFor(@NonNull Computer target) {
121+
return Collections.singletonList(new GraphProxy(target));
122+
}
123+
}
124+
101125
public void doDynamic(StaplerRequest request, StaplerResponse response) throws IOException {
102126
proxy(request, response);
103127
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2018-2019 Matrix Authorization Strategy Plugin developers
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package com.microsoft.jenkins.azuread.integrations.casc;
25+
26+
import com.microsoft.jenkins.azuread.AzureAdAuthorizationMatrixNodeProperty;
27+
import edu.umd.cs.findbugs.annotations.NonNull;
28+
import hudson.Extension;
29+
import io.jenkins.plugins.casc.Attribute;
30+
import io.jenkins.plugins.casc.BaseConfigurator;
31+
import io.jenkins.plugins.casc.ConfigurationContext;
32+
import io.jenkins.plugins.casc.ConfiguratorException;
33+
import io.jenkins.plugins.casc.impl.attributes.DescribableAttribute;
34+
import io.jenkins.plugins.casc.impl.attributes.MultivaluedAttribute;
35+
import io.jenkins.plugins.casc.model.Mapping;
36+
import org.jenkinsci.plugins.matrixauth.inheritance.InheritanceStrategy;
37+
import org.jenkinsci.plugins.matrixauth.integrations.casc.MatrixAuthorizationStrategyConfigurator;
38+
import org.kohsuke.accmod.Restricted;
39+
import org.kohsuke.accmod.restrictions.NoExternalUse;
40+
41+
import java.util.Arrays;
42+
import java.util.HashSet;
43+
import java.util.Set;
44+
45+
@Extension(optional = true)
46+
@Restricted(NoExternalUse.class)
47+
public class AzureAdAuthorizationMatrixNodePropertyConfigurator
48+
extends BaseConfigurator<AzureAdAuthorizationMatrixNodeProperty> {
49+
50+
@Override
51+
public Class<AzureAdAuthorizationMatrixNodeProperty> getTarget() {
52+
return AzureAdAuthorizationMatrixNodeProperty.class;
53+
}
54+
55+
@Override
56+
protected AzureAdAuthorizationMatrixNodeProperty instance(Mapping mapping, ConfigurationContext context)
57+
throws ConfiguratorException {
58+
return new AzureAdAuthorizationMatrixNodeProperty();
59+
}
60+
61+
@Override
62+
@NonNull
63+
public Set<Attribute<AzureAdAuthorizationMatrixNodeProperty, ?>> describe() {
64+
return new HashSet<>(Arrays.asList(
65+
new MultivaluedAttribute<AzureAdAuthorizationMatrixNodeProperty, String>(
66+
"permissions",
67+
String.class
68+
)
69+
.getter(MatrixAuthorizationStrategyConfigurator::getPermissions)
70+
.setter(MatrixAuthorizationStrategyConfigurator::setPermissions),
71+
new DescribableAttribute<AzureAdAuthorizationMatrixNodeProperty,
72+
InheritanceStrategy>("inheritanceStrategy", InheritanceStrategy.class)));
73+
}
74+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.microsoft.jenkins.azuread.AzureAdAuthorizationMatrixNodeProperty
2+
3+
import lib.FormTagLib
4+
import org.jenkinsci.plugins.matrixauth.inheritance.InheritanceStrategyDescriptor
5+
6+
def f = namespace(FormTagLib)
7+
def st = namespace("jelly:stapler")
8+
9+
f.nested {
10+
div {
11+
f.dropdownDescriptorSelector(title: _("Inheritance Strategy"), descriptors: InheritanceStrategyDescriptor.getApplicableDescriptors(my?.class?:hudson.model.Node.class), field: 'inheritanceStrategy')
12+
st.include(class: "com.microsoft.jenkins.azuread.AzureAdMatrixAuthorizationStrategy", page: "config")
13+
}
14+
}

0 commit comments

Comments
 (0)