Skip to content

Commit b0780d1

Browse files
authored
Merge pull request #62 from jenkinsci/jenkins-36315
[FIXED JENKINS-36315] Make it easier to infer the context in form binding
2 parents 4c5d902 + cdcc1c5 commit b0780d1

File tree

10 files changed

+631
-83
lines changed

10 files changed

+631
-83
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.cloudbees.plugins.credentials;
2+
3+
import hudson.model.ModelObject;
4+
import java.lang.annotation.Documented;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.Target;
7+
import javax.servlet.ServletException;
8+
import org.apache.commons.lang.StringUtils;
9+
import org.kohsuke.stapler.AnnotationHandler;
10+
import org.kohsuke.stapler.InjectedParameter;
11+
import org.kohsuke.stapler.StaplerRequest;
12+
13+
import static java.lang.annotation.ElementType.PARAMETER;
14+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
15+
16+
/**
17+
* Indicates that this parameter is injected by evaluating
18+
* {@link StaplerRequest#getAncestors()} and searching for a credentials context with the parameter type.
19+
* You can enhance the lookup by ensuring that there are query parameters of {@code $provider} and {@code $token}
20+
* that correspond to the context's {@link CredentialsSelectHelper.ContextResolver} FQCN and
21+
* {@link CredentialsSelectHelper.ContextResolver#getToken(ModelObject)} respectively.
22+
*
23+
* @see CredentialsDescriptor#getCheckMethod(String)
24+
* @since 2.1.5
25+
*/
26+
@Retention(RUNTIME)
27+
@Target(PARAMETER)
28+
@Documented
29+
@InjectedParameter(ContextInPath.HandlerImpl.class)
30+
public @interface ContextInPath {
31+
class HandlerImpl extends AnnotationHandler<ContextInPath> {
32+
public Object parse(StaplerRequest request, ContextInPath contextInPath, Class type, String parameterName)
33+
throws
34+
ServletException {
35+
String $provider = request.getParameter("$provider");
36+
String $token = request.getParameter("$token");
37+
if (StringUtils.isNotBlank($provider) && StringUtils.isNotBlank($token)) {
38+
ModelObject context = CredentialsDescriptor.lookupContext($provider, $token);
39+
if (type.isInstance(context)) {
40+
return type.cast(context);
41+
}
42+
}
43+
return CredentialsDescriptor.findContextInPath(request, type);
44+
}
45+
}
46+
}

src/main/java/com/cloudbees/plugins/credentials/CredentialsDescriptor.java

Lines changed: 433 additions & 16 deletions
Large diffs are not rendered by default.

src/main/java/com/cloudbees/plugins/credentials/CredentialsProvider.java

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -599,12 +599,7 @@ public static <C extends IdCredentials> ListBoxModel listCredentials(@NonNull Cl
599599
*/
600600
@CheckForNull
601601
public static Set<CredentialsScope> lookupScopes(ModelObject object) {
602-
if (object instanceof CredentialsStoreAction.CredentialsWrapper) {
603-
object = ((CredentialsStoreAction.CredentialsWrapper) object).getStore().getContext();
604-
}
605-
if (object instanceof CredentialsStoreAction.DomainWrapper) {
606-
object = ((CredentialsStoreAction.DomainWrapper) object).getStore().getContext();
607-
}
602+
object = CredentialsDescriptor.unwrapContext(object);
608603
Set<CredentialsScope> result = null;
609604
for (CredentialsProvider provider : all()) {
610605
if (provider.isEnabled(object)) {
@@ -625,21 +620,37 @@ public static Set<CredentialsScope> lookupScopes(ModelObject object) {
625620
return result;
626621
}
627622

623+
/**
624+
* Tests if the supplied context has any credentials stores associated with it.
625+
*
626+
* @param context the context object.
627+
* @return {@code true} if and only if the supplied context has at least one {@link CredentialsStore} associated
628+
* with it.
629+
* @since 2.1.5
630+
*/
631+
public static boolean hasStores(final ModelObject context) {
632+
for (CredentialsProvider p : all()) {
633+
if (p.isEnabled(context) && p.getStore(context) != null) {
634+
return true;
635+
}
636+
}
637+
return false;
638+
}
639+
628640
/**
629641
* Returns a lazy {@link Iterable} of all the {@link CredentialsStore} instances contributing credentials to the
630-
* supplied
631-
* object.
642+
* supplied object.
632643
*
633-
* @param object the {@link Item} or {@link ItemGroup} or {@link User} to get the {@link CredentialsStore}s of.
644+
* @param context the {@link Item} or {@link ItemGroup} or {@link User} to get the {@link CredentialsStore}s of.
634645
* @return a lazy {@link Iterable} of all {@link CredentialsStore} instances.
635646
* @since 1.8
636647
*/
637-
public static Iterable<CredentialsStore> lookupStores(final ModelObject object) {
648+
public static Iterable<CredentialsStore> lookupStores(final ModelObject context) {
638649
final ExtensionList<CredentialsProvider> providers = all();
639650
return new Iterable<CredentialsStore>() {
640651
public Iterator<CredentialsStore> iterator() {
641652
return new Iterator<CredentialsStore>() {
642-
private ModelObject current = object;
653+
private ModelObject current = context;
643654
private Iterator<CredentialsProvider> iterator = providers.iterator();
644655
private CredentialsStore next;
645656

@@ -650,7 +661,7 @@ public boolean hasNext() {
650661
while (current != null) {
651662
while (iterator.hasNext()) {
652663
CredentialsProvider p = iterator.next();
653-
if (!p.isEnabled(object)) {
664+
if (!p.isEnabled(context)) {
654665
continue;
655666
}
656667
next = p.getStore(current);

src/main/java/com/cloudbees/plugins/credentials/CredentialsStore.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.util.Collections;
4949
import java.util.Iterator;
5050
import java.util.List;
51+
import java.util.Set;
5152
import jenkins.model.Jenkins;
5253
import org.acegisecurity.Authentication;
5354
import org.apache.commons.lang.StringUtils;
@@ -142,6 +143,19 @@ public final CredentialsProvider getProvider() {
142143
return ExtensionList.lookup(CredentialsProvider.class).get(providerClass);
143144
}
144145

146+
/**
147+
* Returns the {@link CredentialsScope} instances that are applicable to this store.
148+
* @return the {@link CredentialsScope} instances that are applicable to this store or {@code null} if the store
149+
* instance is no longer enabled.
150+
*
151+
* @since 2.1.5
152+
*/
153+
@Nullable
154+
public final Set<CredentialsScope> getScopes() {
155+
CredentialsProvider provider = getProvider();
156+
return provider == null ? null : provider.getScopes(getContext());
157+
}
158+
145159
/**
146160
* Returns the context within which this store operates. Credentials in this store will be available to
147161
* child contexts (unless {@link CredentialsScope#SYSTEM} is valid for the store) but will not be available to

src/main/java/com/cloudbees/plugins/credentials/impl/BaseStandardCredentials.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,39 @@
2424
package com.cloudbees.plugins.credentials.impl;
2525

2626
import com.cloudbees.plugins.credentials.BaseCredentials;
27+
import com.cloudbees.plugins.credentials.ContextInPath;
28+
import com.cloudbees.plugins.credentials.Credentials;
2729
import com.cloudbees.plugins.credentials.CredentialsDescriptor;
30+
import com.cloudbees.plugins.credentials.CredentialsMatcher;
2831
import com.cloudbees.plugins.credentials.CredentialsMatchers;
2932
import com.cloudbees.plugins.credentials.CredentialsProvider;
3033
import com.cloudbees.plugins.credentials.CredentialsScope;
34+
import com.cloudbees.plugins.credentials.CredentialsSelectHelper;
3135
import com.cloudbees.plugins.credentials.CredentialsStore;
36+
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
3237
import com.cloudbees.plugins.credentials.common.IdCredentials;
3338
import com.cloudbees.plugins.credentials.common.StandardCredentials;
3439
import com.cloudbees.plugins.credentials.domains.Domain;
3540
import edu.umd.cs.findbugs.annotations.CheckForNull;
3641
import edu.umd.cs.findbugs.annotations.NonNull;
42+
import hudson.ExtensionList;
3743
import hudson.Util;
3844
import hudson.model.Item;
3945
import hudson.model.ModelObject;
4046
import hudson.model.User;
4147
import hudson.util.FormValidation;
48+
import java.io.UnsupportedEncodingException;
49+
import java.net.URLEncoder;
50+
import java.util.EnumSet;
51+
import java.util.Set;
4252
import jenkins.model.Jenkins;
43-
import org.kohsuke.stapler.AncestorInPath;
53+
import org.apache.commons.lang.StringUtils;
4454
import org.kohsuke.stapler.QueryParameter;
4555
import org.kohsuke.stapler.export.Exported;
4656
import org.kohsuke.stapler.export.ExportedBean;
4757

58+
import static com.cloudbees.plugins.credentials.CredentialsSelectHelper.*;
59+
4860
/**
4961
* Base class for {@link StandardCredentials}.
5062
*/
@@ -139,17 +151,22 @@ protected BaseStandardCredentialsDescriptor(Class<? extends BaseStandardCredenti
139151

140152
@CheckForNull
141153
private static FormValidation checkForDuplicates(String value, ModelObject context, ModelObject object) {
154+
CredentialsMatcher withId = CredentialsMatchers.withId(value);
142155
for (CredentialsStore store : CredentialsProvider.lookupStores(object)) {
143156
if (!store.hasPermission(CredentialsProvider.VIEW)) {
144157
continue;
145158
}
146159
ModelObject storeContext = store.getContext();
147160
for (Domain domain : store.getDomains()) {
148-
if (CredentialsMatchers.firstOrNull(store.getCredentials(domain), CredentialsMatchers.withId(value))
149-
!= null) {
161+
for (Credentials match : CredentialsMatchers.filter(store.getCredentials(domain), withId)) {
150162
if (storeContext == context) {
151163
return FormValidation.error("This ID is already in use");
152164
} else {
165+
CredentialsScope scope = match.getScope();
166+
if (scope != null && !scope.isVisible(context)) {
167+
// scope is not exported to child contexts
168+
continue;
169+
}
153170
return FormValidation.warning("The ID ‘%s’ is already in use in %s", value,
154171
storeContext instanceof Item
155172
? ((Item) storeContext).getFullDisplayName()
@@ -161,7 +178,28 @@ private static FormValidation checkForDuplicates(String value, ModelObject conte
161178
return null;
162179
}
163180

164-
public final FormValidation doCheckId(@QueryParameter String value, @AncestorInPath ModelObject context) {
181+
/**
182+
* Gets the check id url for the specified store.
183+
*
184+
* @param store the store.
185+
* @return the url of the id check endpoint.
186+
* @throws UnsupportedEncodingException if the JVM does not implement the JLS.
187+
*/
188+
public String getCheckIdUrl(CredentialsStore store) throws UnsupportedEncodingException {
189+
ModelObject context = store.getContext();
190+
for (ContextResolver r : ExtensionList.lookup(ContextResolver.class)) {
191+
String token = r.getToken(context);
192+
if (token != null) {
193+
return Jenkins.getActiveInstance().getRootUrlFromRequest() + "/" + getDescriptorUrl()
194+
+ "/checkId?provider=" + r.getClass().getName() + "&token="
195+
+ URLEncoder.encode(token, "UTF-8");
196+
}
197+
}
198+
return Jenkins.getActiveInstance().getRootUrlFromRequest() + "/" + getDescriptorUrl()
199+
+ "/checkId?provider=null&token=null";
200+
}
201+
202+
public final FormValidation doCheckId(@ContextInPath ModelObject context, @QueryParameter String value) {
165203
if (value.isEmpty()) {
166204
return FormValidation.ok();
167205
}

src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential.jelly

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,24 @@
2424
-->
2525
<?jelly escape-by-default='true'?>
2626
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout">
27-
<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
28-
<j:set var="descriptor" value="${it.credentialDescriptor}"/>
29-
<j:set var="instance" value="${null}"/>
30-
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
31-
<j:set var="current" value="${instance[credentials]}"/>
32-
<j:set var="current" value="${current!=null ? current : null}"/>
33-
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
34-
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
35-
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}" staplerClass="${descriptor.clazz.name}"
36-
lazy="descriptor,it">
37-
<l:ajax>
38-
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}" />
39-
<st:include from="${descriptor}" page="${descriptor.configPage}" />
40-
</l:ajax>
41-
</f:dropdownListBlock>
42-
</j:forEach>
43-
</f:dropdownList>
27+
<j:local>
28+
<j:set var="it" value="${it.store}"/>
29+
<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
30+
<j:set var="descriptor" value="${it.credentialDescriptor}"/>
31+
<j:set var="instance" value="${null}"/>
32+
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
33+
<j:set var="current" value="${instance[credentials]}"/>
34+
<j:set var="current" value="${current!=null ? current : null}"/>
35+
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
36+
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
37+
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}" staplerClass="${descriptor.clazz.name}"
38+
lazy="descriptor,it">
39+
<l:ajax>
40+
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}" />
41+
<st:include from="${descriptor}" page="${descriptor.configPage}" />
42+
</l:ajax>
43+
</f:dropdownListBlock>
44+
</j:forEach>
45+
</f:dropdownList>
46+
</j:local>
4447
</j:jelly>

src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/CredentialsWrapper/update.jelly

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,20 @@
3434
<l:main-panel>
3535
<j:set var="descriptor" value="${it.descriptor}"/>
3636
<j:set var="instance" value="${it}"/>
37-
<f:form action="updateSubmit" method="POST" name="update">
38-
<j:set var="instance" value="${instance.credentials}" />
39-
<j:set var="descriptor" value="${instance.descriptor}"/>
40-
<f:invisibleEntry>
41-
<input type="hidden" name="stapler-class" value="${descriptor.clazz.name}" />
42-
</f:invisibleEntry>
43-
<st:include from="${descriptor}" page="${descriptor.configPage}" />
44-
<f:bottomButtonBar>
45-
<f:submit value="${%Save}"/>
46-
</f:bottomButtonBar>
47-
</f:form>
37+
<j:local>
38+
<j:set var="it" value="${it.store}"/>
39+
<f:form action="updateSubmit" method="POST" name="update">
40+
<j:set var="instance" value="${instance.credentials}" />
41+
<j:set var="descriptor" value="${instance.descriptor}"/>
42+
<f:invisibleEntry>
43+
<input type="hidden" name="stapler-class" value="${descriptor.clazz.name}" />
44+
</f:invisibleEntry>
45+
<st:include from="${descriptor}" page="${descriptor.configPage}" />
46+
<f:bottomButtonBar>
47+
<f:submit value="${%Save}"/>
48+
</f:bottomButtonBar>
49+
</f:form>
50+
</j:local>
4851
<script>
4952
// TODO remove this JENKINS-24662 workaround when baseline core has fix for root cause
5053
window.setTimeout(function(){layoutUpdateCallback.call();}, 1000);

src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,26 @@
4545
</f:rowSet>
4646
</j:when>
4747
<j:otherwise>
48-
<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
49-
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
50-
<j:set var="current" value="${instance[credentials]}"/>
51-
<j:set var="current" value="${current!=null ? current : null}"/>
52-
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
53-
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
54-
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}"
55-
staplerClass="${descriptor.clazz.name}"
56-
lazy="descriptor,it">
57-
<l:ajax>
58-
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}"/>
59-
<st:include from="${descriptor}" page="${descriptor.configPage}"/>
60-
</l:ajax>
61-
</f:dropdownListBlock>
62-
</j:forEach>
63-
</f:dropdownList>
64-
<!--f:dropdownDescriptorSelector field="credentials" title="${%Kind}" lazy="it"/-->
48+
<j:local>
49+
<j:set var="it" value="${it.store}"/>
50+
<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
51+
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
52+
<j:set var="current" value="${instance[credentials]}"/>
53+
<j:set var="current" value="${current!=null ? current : null}"/>
54+
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
55+
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
56+
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}"
57+
staplerClass="${descriptor.clazz.name}"
58+
lazy="descriptor,it">
59+
<l:ajax>
60+
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}"/>
61+
<st:include from="${descriptor}" page="${descriptor.configPage}"/>
62+
</l:ajax>
63+
</f:dropdownListBlock>
64+
</j:forEach>
65+
</f:dropdownList>
66+
<!--f:dropdownDescriptorSelector field="credentials" title="${%Kind}" lazy="it"/-->
67+
</j:local>
6568
</j:otherwise>
6669
</j:choose>
6770

src/main/resources/com/cloudbees/plugins/credentials/impl/BaseStandardCredentials/id-and-description.jelly

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
<!-- But there appears to be no way to turn off checkUrl when field is specified. -->
2929
<!-- So the only apparent workaround is to disable field when instance != null, and code name and value manually. -->
3030
<f:entry field="${instance != null ? null : 'id'}" title="${%ID}">
31-
<f:textbox name="_.id" value="${instance != null ? instance.id : null}" readonly="${instance != null ? 'readonly' : null}"/>
31+
<f:textbox name="_.id" value="${instance != null ? instance.id : null}"
32+
readonly="${instance != null ? 'readonly' : null}"
33+
checkUrl="${descriptor.getCheckUrl('id')}"
34+
/>
3235
</f:entry>
3336
<f:entry title="${%Description}" field="description">
3437
<f:textbox/>

0 commit comments

Comments
 (0)