Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.cloudbees.plugins.credentials;

import hudson.model.ModelObject;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.servlet.ServletException;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.AnnotationHandler;
import org.kohsuke.stapler.InjectedParameter;
import org.kohsuke.stapler.StaplerRequest;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Indicates that this parameter is injected by evaluating
* {@link StaplerRequest#getAncestors()} and searching for a credentials context with the parameter type.
* You can enhance the lookup by ensuring that there are query parameters of {@code $provider} and {@code $token}
* that correspond to the context's {@link CredentialsSelectHelper.ContextResolver} FQCN and
* {@link CredentialsSelectHelper.ContextResolver#getToken(ModelObject)} respectively.
*
* @see CredentialsDescriptor#getCheckMethod(String)
* @since 2.1.5
*/
@Retention(RUNTIME)
@Target(PARAMETER)
@Documented
@InjectedParameter(ContextInPath.HandlerImpl.class)
public @interface ContextInPath {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jglick you may be interested in this as a less hacky approach to https://github.com/jenkinsci/workflow-step-api-plugin/blob/aabbdca1c3c1f1aacf346b01d95d3be467547195/src/main/java/org/jenkinsci/plugins/workflow/util/StaplerReferer.java#L43

I wonder could we introduce some extension points and make a more generic solution in core?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linked from JENKINS-19413.

class HandlerImpl extends AnnotationHandler<ContextInPath> {
public Object parse(StaplerRequest request, ContextInPath contextInPath, Class type, String parameterName)
throws
ServletException {
String $provider = request.getParameter("$provider");
String $token = request.getParameter("$token");
if (StringUtils.isNotBlank($provider) && StringUtils.isNotBlank($token)) {
ModelObject context = CredentialsDescriptor.lookupContext($provider, $token);
if (type.isInstance(context)) {
return type.cast(context);
}
}
return CredentialsDescriptor.findContextInPath(request, type);
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -599,12 +599,7 @@ public static <C extends IdCredentials> ListBoxModel listCredentials(@NonNull Cl
*/
@CheckForNull
public static Set<CredentialsScope> lookupScopes(ModelObject object) {
if (object instanceof CredentialsStoreAction.CredentialsWrapper) {
object = ((CredentialsStoreAction.CredentialsWrapper) object).getStore().getContext();
}
if (object instanceof CredentialsStoreAction.DomainWrapper) {
object = ((CredentialsStoreAction.DomainWrapper) object).getStore().getContext();
}
object = CredentialsDescriptor.unwrapContext(object);
Set<CredentialsScope> result = null;
for (CredentialsProvider provider : all()) {
if (provider.isEnabled(object)) {
Expand All @@ -625,21 +620,37 @@ public static Set<CredentialsScope> lookupScopes(ModelObject object) {
return result;
}

/**
* Tests if the supplied context has any credentials stores associated with it.
*
* @param context the context object.
* @return {@code true} if and only if the supplied context has at least one {@link CredentialsStore} associated
* with it.
* @since 2.1.5
*/
public static boolean hasStores(final ModelObject context) {
for (CredentialsProvider p : all()) {
if (p.isEnabled(context) && p.getStore(context) != null) {
return true;
}
}
return false;
}

/**
* Returns a lazy {@link Iterable} of all the {@link CredentialsStore} instances contributing credentials to the
* supplied
* object.
* supplied object.
*
* @param object the {@link Item} or {@link ItemGroup} or {@link User} to get the {@link CredentialsStore}s of.
* @param context the {@link Item} or {@link ItemGroup} or {@link User} to get the {@link CredentialsStore}s of.
* @return a lazy {@link Iterable} of all {@link CredentialsStore} instances.
* @since 1.8
*/
public static Iterable<CredentialsStore> lookupStores(final ModelObject object) {
public static Iterable<CredentialsStore> lookupStores(final ModelObject context) {
final ExtensionList<CredentialsProvider> providers = all();
return new Iterable<CredentialsStore>() {
public Iterator<CredentialsStore> iterator() {
return new Iterator<CredentialsStore>() {
private ModelObject current = object;
private ModelObject current = context;
private Iterator<CredentialsProvider> iterator = providers.iterator();
private CredentialsStore next;

Expand All @@ -650,7 +661,7 @@ public boolean hasNext() {
while (current != null) {
while (iterator.hasNext()) {
CredentialsProvider p = iterator.next();
if (!p.isEnabled(object)) {
if (!p.isEnabled(context)) {
continue;
}
next = p.getStore(current);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.apache.commons.lang.StringUtils;
Expand Down Expand Up @@ -142,6 +143,19 @@ public final CredentialsProvider getProvider() {
return ExtensionList.lookup(CredentialsProvider.class).get(providerClass);
}

/**
* Returns the {@link CredentialsScope} instances that are applicable to this store.
* @return the {@link CredentialsScope} instances that are applicable to this store or {@code null} if the store
* instance is no longer enabled.
*
* @since 2.1.5
*/
@Nullable
public final Set<CredentialsScope> getScopes() {
CredentialsProvider provider = getProvider();
return provider == null ? null : provider.getScopes(getContext());
}

/**
* Returns the context within which this store operates. Credentials in this store will be available to
* child contexts (unless {@link CredentialsScope#SYSTEM} is valid for the store) but will not be available to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,39 @@
package com.cloudbees.plugins.credentials.impl;

import com.cloudbees.plugins.credentials.BaseCredentials;
import com.cloudbees.plugins.credentials.ContextInPath;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsDescriptor;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsSelectHelper;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
import com.cloudbees.plugins.credentials.common.IdCredentials;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionList;
import hudson.Util;
import hudson.model.Item;
import hudson.model.ModelObject;
import hudson.model.User;
import hudson.util.FormValidation;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.EnumSet;
import java.util.Set;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.AncestorInPath;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

import static com.cloudbees.plugins.credentials.CredentialsSelectHelper.*;

/**
* Base class for {@link StandardCredentials}.
*/
Expand Down Expand Up @@ -139,17 +151,22 @@ protected BaseStandardCredentialsDescriptor(Class<? extends BaseStandardCredenti

@CheckForNull
private static FormValidation checkForDuplicates(String value, ModelObject context, ModelObject object) {
CredentialsMatcher withId = CredentialsMatchers.withId(value);
for (CredentialsStore store : CredentialsProvider.lookupStores(object)) {
if (!store.hasPermission(CredentialsProvider.VIEW)) {
continue;
}
ModelObject storeContext = store.getContext();
for (Domain domain : store.getDomains()) {
if (CredentialsMatchers.firstOrNull(store.getCredentials(domain), CredentialsMatchers.withId(value))
!= null) {
for (Credentials match : CredentialsMatchers.filter(store.getCredentials(domain), withId)) {
if (storeContext == context) {
return FormValidation.error("This ID is already in use");
} else {
CredentialsScope scope = match.getScope();
if (scope != null && !scope.isVisible(context)) {
// scope is not exported to child contexts
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Should it continue with other credentials in the domain then? firstOrNull skips others from what I see

}
return FormValidation.warning("The ID ‘%s’ is already in use in %s", value,
storeContext instanceof Item
? ((Item) storeContext).getFullDisplayName()
Expand All @@ -161,7 +178,28 @@ private static FormValidation checkForDuplicates(String value, ModelObject conte
return null;
}

public final FormValidation doCheckId(@QueryParameter String value, @AncestorInPath ModelObject context) {
/**
* Gets the check id url for the specified store.
*
* @param store the store.
* @return the url of the id check endpoint.
* @throws UnsupportedEncodingException if the JVM does not implement the JLS.
*/
public String getCheckIdUrl(CredentialsStore store) throws UnsupportedEncodingException {
ModelObject context = store.getContext();
for (ContextResolver r : ExtensionList.lookup(ContextResolver.class)) {
String token = r.getToken(context);
if (token != null) {
return Jenkins.getActiveInstance().getRootUrlFromRequest() + "/" + getDescriptorUrl()
+ "/checkId?provider=" + r.getClass().getName() + "&token="
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐜 class name escaping

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

talk to core! (but I'll switch to sharing core's bug so I can share in core's fix)

+ URLEncoder.encode(token, "UTF-8");
}
}
return Jenkins.getActiveInstance().getRootUrlFromRequest() + "/" + getDescriptorUrl()
+ "/checkId?provider=null&token=null";
}

public final FormValidation doCheckId(@ContextInPath ModelObject context, @QueryParameter String value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restricted?

if (value.isEmpty()) {
return FormValidation.ok();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,24 @@
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
<j:set var="descriptor" value="${it.credentialDescriptor}"/>
<j:set var="instance" value="${null}"/>
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
<j:set var="current" value="${instance[credentials]}"/>
<j:set var="current" value="${current!=null ? current : null}"/>
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}" staplerClass="${descriptor.clazz.name}"
lazy="descriptor,it">
<l:ajax>
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}" />
<st:include from="${descriptor}" page="${descriptor.configPage}" />
</l:ajax>
</f:dropdownListBlock>
</j:forEach>
</f:dropdownList>
<j:local>
<j:set var="it" value="${it.store}"/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need j:local here? AFAIK it would have been scoped only to this include anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about this j:local -- it bleeds into the html that web browsers see. That isn't inherently bad, but in general one shouldn't bleed xml namespaced tags to html web browser.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn’t, assuming the xml namespace is correctly defined

Copy link
Member

@slide slide Feb 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a <local> tag in the Jelly documentation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh should be scope http://commons.apache.org/proper/commons-jelly/tags.html#core:scope Do you want to fix it?

<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
<j:set var="descriptor" value="${it.credentialDescriptor}"/>
<j:set var="instance" value="${null}"/>
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
<j:set var="current" value="${instance[credentials]}"/>
<j:set var="current" value="${current!=null ? current : null}"/>
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}" staplerClass="${descriptor.clazz.name}"
lazy="descriptor,it">
<l:ajax>
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}" />
<st:include from="${descriptor}" page="${descriptor.configPage}" />
</l:ajax>
</f:dropdownListBlock>
</j:forEach>
</f:dropdownList>
</j:local>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@
<l:main-panel>
<j:set var="descriptor" value="${it.descriptor}"/>
<j:set var="instance" value="${it}"/>
<f:form action="updateSubmit" method="POST" name="update">
<j:set var="instance" value="${instance.credentials}" />
<j:set var="descriptor" value="${instance.descriptor}"/>
<f:invisibleEntry>
<input type="hidden" name="stapler-class" value="${descriptor.clazz.name}" />
</f:invisibleEntry>
<st:include from="${descriptor}" page="${descriptor.configPage}" />
<f:bottomButtonBar>
<f:submit value="${%Save}"/>
</f:bottomButtonBar>
</f:form>
<j:local>
<j:set var="it" value="${it.store}"/>
<f:form action="updateSubmit" method="POST" name="update">
<j:set var="instance" value="${instance.credentials}" />
<j:set var="descriptor" value="${instance.descriptor}"/>
<f:invisibleEntry>
<input type="hidden" name="stapler-class" value="${descriptor.clazz.name}" />
</f:invisibleEntry>
<st:include from="${descriptor}" page="${descriptor.configPage}" />
<f:bottomButtonBar>
<f:submit value="${%Save}"/>
</f:bottomButtonBar>
</f:form>
</j:local>
<script>
// TODO remove this JENKINS-24662 workaround when baseline core has fix for root cause
window.setTimeout(function(){layoutUpdateCallback.call();}, 1000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,26 @@
</f:rowSet>
</j:when>
<j:otherwise>
<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
<j:set var="current" value="${instance[credentials]}"/>
<j:set var="current" value="${current!=null ? current : null}"/>
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}"
staplerClass="${descriptor.clazz.name}"
lazy="descriptor,it">
<l:ajax>
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}"/>
<st:include from="${descriptor}" page="${descriptor.configPage}"/>
</l:ajax>
</f:dropdownListBlock>
</j:forEach>
</f:dropdownList>
<!--f:dropdownDescriptorSelector field="credentials" title="${%Kind}" lazy="it"/-->
<j:local>
<j:set var="it" value="${it.store}"/>
<!-- TODO revert to dropdownDescriptorSelector when baseline is 1.645+ which supports `capture` attribute -->
<f:dropdownList name="credentials" title="${%Kind}" help="${descriptor.getHelpFile('credentials')}">
<j:set var="current" value="${instance[credentials]}"/>
<j:set var="current" value="${current!=null ? current : null}"/>
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
<f:dropdownListBlock value="${loop.index}" title="${descriptor.displayName}"
selected="${current.descriptor==descriptor || (current==null and descriptor==attrs.default)}"
staplerClass="${descriptor.clazz.name}"
lazy="descriptor,it">
<l:ajax>
<j:set var="instance" value="${current.descriptor==descriptor ? current : null}"/>
<st:include from="${descriptor}" page="${descriptor.configPage}"/>
</l:ajax>
</f:dropdownListBlock>
</j:forEach>
</f:dropdownList>
<!--f:dropdownDescriptorSelector field="credentials" title="${%Kind}" lazy="it"/-->
</j:local>
</j:otherwise>
</j:choose>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
<!-- But there appears to be no way to turn off checkUrl when field is specified. -->
<!-- So the only apparent workaround is to disable field when instance != null, and code name and value manually. -->
<f:entry field="${instance != null ? null : 'id'}" title="${%ID}">
<f:textbox name="_.id" value="${instance != null ? instance.id : null}" readonly="${instance != null ? 'readonly' : null}"/>
<f:textbox name="_.id" value="${instance != null ? instance.id : null}"
readonly="${instance != null ? 'readonly' : null}"
checkUrl="${descriptor.getCheckUrl('id')}"
/>
</f:entry>
<f:entry title="${%Description}" field="description">
<f:textbox/>
Expand Down
Loading