Java tutorial
/* * The MIT License * * Copyright (c) 2013-2016, CloudBees, Inc., Stephen Connolly. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.cloudbees.plugins.credentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.Util; import hudson.cli.declarative.CLIResolver; import hudson.model.ComputerSet; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Item; import hudson.model.ModelObject; import hudson.model.User; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.util.FormApply; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import javax.servlet.ServletException; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.jenkins.ui.icon.IconSpec; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.Localizable; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.interceptor.RequirePOST; /** * A descriptor used to assist the c:select tag with allowing in-place addition of credentials. * * @author Stephen Connolly */ @Extension public class CredentialsSelectHelper extends Descriptor<CredentialsSelectHelper> implements Describable<CredentialsSelectHelper> { /** * Expose the {@link CredentialsProvider#CREATE} permission for Jelly. */ public static final Permission CREATE = CredentialsProvider.CREATE; /** * {@inheritDoc} */ public CredentialsSelectHelper() { super(CredentialsSelectHelper.class); } /** * {@inheritDoc} */ public CredentialsSelectHelper getDescriptor() { return this; } /** * {@inheritDoc} */ @Override public String getDisplayName() { return Messages.CredentialsSelectHelper_DisplayName(); } /** * Fixes up the context in case we are called from a page where the context is not a ModelObject. * * @param context the initial guess of the context. * @return the resolved context. * @since 2.0.7 */ @CheckForNull @Restricted(NoExternalUse.class) public ModelObject resolveContext(Object context) { if (context instanceof ModelObject) { return (ModelObject) context; } StaplerRequest request = Stapler.getCurrentRequest(); if (request != null) { return request.findAncestorObject(ModelObject.class); } return null; } /** * Returns the {@link StoreItem} instances for the current Stapler request. * * @param context the context in which to retrieve the store items. * @param includeUser {@code true} to also include any User scoped stores. * @return the {@link StoreItem} instances for the current Stapler request. * @since 2.0.5 */ @Restricted(NoExternalUse.class) public List<StoreItem> getStoreItems(ModelObject context, boolean includeUser) { Set<String> urls = new HashSet<String>(); List<StoreItem> result = new ArrayList<StoreItem>(); if (context == null) { StaplerRequest request = Stapler.getCurrentRequest(); if (request != null) { context = request.findAncestorObject(ModelObject.class); } } if (context != null) { for (CredentialsStore store : CredentialsProvider.lookupStores(context)) { StoreItem item = new StoreItem(store); String url = item.getUrl(); if (item.getUrl() != null && !urls.contains(url)) { result.add(item); urls.add(url); } } } if (includeUser) { boolean hasPermission = false; ModelObject current = context; while (current != null) { if (current instanceof AccessControlled) { hasPermission = ((AccessControlled) current).hasPermission(CredentialsProvider.USE_OWN); break; } else if (current instanceof ComputerSet) { current = Jenkins.getActiveInstance(); } else { // fall back to Jenkins as the ultimate parent of everything else current = Jenkins.getActiveInstance(); } } if (hasPermission) { for (CredentialsStore store : CredentialsProvider.lookupStores(User.current())) { StoreItem item = new StoreItem(store); String url = item.getUrl(); if (item.getUrl() != null && !urls.contains(url)) { result.add(item); urls.add(url); } } } } return result; } /** * Checks if the current user has permission to create a credential. * * @param context the context. * @param includeUser whether they can use their own credentials store. * @return {@code true} if they can create a permission. * @since FIXME */ @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // used via jelly public boolean hasCreatePermission(ModelObject context, boolean includeUser) { if (includeUser) { User current = User.current(); if (current != null && current.hasPermission(CREATE)) { return true; } } if (context == null) { StaplerRequest request = Stapler.getCurrentRequest(); if (request != null) { context = request.findAncestorObject(ModelObject.class); } } for (CredentialsStore store : CredentialsProvider.lookupStores(context)) { if (store.hasPermission(CREATE)) { return true; } } return false; } /** * Stapler binding for the resolver URL segment. * * @param className the class name of the resolver. * @return the wrapped resolver. * @since 2.0 */ @Restricted(NoExternalUse.class) public WrappedContextResolver getResolver(String className) { for (ContextResolver r : ExtensionList.lookup(ContextResolver.class)) { if (r.getClass().getName().equals(className)) { return new WrappedContextResolver(r); } } return null; } /** * Resolves a {@link CredentialsStore} instance for CLI commands. * * @param storeId the store identifier. * @return the {@link CredentialsStore} instance. * @throws CmdLineException if the store cannot be resolved. * @since 2.1.1 */ @CLIResolver public static CredentialsStore resolveForCLI( @Argument(required = true, metaVar = "STORE", usage = "Store ID") String storeId) throws CmdLineException { int index1 = storeId.indexOf("::"); int index2 = index1 == -1 ? -1 : storeId.indexOf("::", index1 + 2); if (index1 == -1 || index1 == 0 || index2 == -1 || index2 < (index1 + 2) || index2 == storeId.length() - 2) { throw new CmdLineException(null, new Localizable() { @Override public String formatWithLocale(Locale locale, Object... objects) { return Messages._CredentialsSelectHelper_CLIMalformedStoreId(objects[0]).toString(locale); } @Override public String format(Object... objects) { return Messages._CredentialsSelectHelper_CLIMalformedStoreId(objects[0]).toString(); } }, storeId); } String providerName = storeId.substring(0, index1); String resolverName = storeId.substring(index1 + 2, index2); String token = storeId.substring(index2 + 2); CredentialsProvider provider = getProvidersByName().get(providerName); if (provider == null || provider == CredentialsProvider.NONE) { throw new CmdLineException(null, new Localizable() { @Override public String formatWithLocale(Locale locale, Object... objects) { return Messages._CredentialsSelectHelper_CLINoSuchProvider(objects[0]).toString(locale); } @Override public String format(Object... objects) { return Messages._CredentialsSelectHelper_CLINoSuchProvider(objects[0]).toString(); } }, storeId); } ContextResolver resolver = getResolversByName().get(resolverName); if (resolver == null || resolver == ContextResolver.NONE) { throw new CmdLineException(null, new Localizable() { @Override public String formatWithLocale(Locale locale, Object... objects) { return Messages._CredentialsSelectHelper_CLINoSuchResolver(objects[0]).toString(locale); } @Override public String format(Object... objects) { return Messages._CredentialsSelectHelper_CLINoSuchResolver(objects[0]).toString(); } }, storeId); } ModelObject context = resolver.getContext(token); if (context == null) { throw new CmdLineException(null, new Localizable() { @Override public String formatWithLocale(Locale locale, Object... objects) { return Messages._CredentialsSelectHelper_CLINoSuchContext(objects[0]).toString(locale); } @Override public String format(Object... objects) { return Messages._CredentialsSelectHelper_CLINoSuchContext(objects[0]).toString(); } }, storeId); } CredentialsStore store = provider.getStore(context); if (store == null) { throw new CmdLineException(null, new Localizable() { @Override public String formatWithLocale(Locale locale, Object... objects) { return Messages._CredentialsSelectHelper_CLINoStore().toString(locale); } @Override public String format(Object... objects) { return Messages._CredentialsSelectHelper_CLINoStore().toString(); } }, storeId); } return store; } /** * Returns a map of the {@link ContextResolver} instances keyed by their name. A resolver may have more than one * entry if there are inferred unique short nicknames. * * @return a map of the {@link ContextResolver} instances keyed by their name * @since 2.1.1 */ public static Map<String, ContextResolver> getResolversByName() { Map<String, ContextResolver> resolverByName = new TreeMap<String, ContextResolver>(); for (ContextResolver r : ExtensionList.lookup(ContextResolver.class)) { resolverByName.put(r.getClass().getName(), r); String shortName = r.getClass().getSimpleName(); resolverByName.put(shortName, resolverByName.containsKey(shortName) ? ContextResolver.NONE : r); shortName = shortName.toLowerCase(Locale.ENGLISH).replaceAll("(context|resolver|impl)*", ""); if (StringUtils.isNotBlank(shortName)) { resolverByName.put(shortName, resolverByName.containsKey(shortName) ? ContextResolver.NONE : r); } } for (Iterator<ContextResolver> iterator = resolverByName.values().iterator(); iterator.hasNext();) { ContextResolver r = iterator.next(); if (r == ContextResolver.NONE) { iterator.remove(); } } return resolverByName; } /** * Returns a map of the {@link CredentialsProvider} instances keyed by their name. A provider may have more than one * entry if there are inferred unique short nicknames. * * @return a map of the {@link CredentialsProvider} instances keyed by their name * @since 2.1.1 */ public static Map<String, CredentialsProvider> getProvidersByName() { Map<String, CredentialsProvider> providerByName = new TreeMap<String, CredentialsProvider>(); for (CredentialsProvider r : ExtensionList.lookup(CredentialsProvider.class)) { providerByName.put(r.getClass().getName(), r); Class<?> clazz = r.getClass(); while (clazz != null) { String shortName = clazz.getSimpleName(); clazz = clazz.getEnclosingClass(); String simpleName = shortName.toLowerCase(Locale.ENGLISH).replaceAll("(credentials|provider|impl)*", ""); if (StringUtils.isBlank(simpleName)) continue; providerByName.put(shortName, providerByName.containsKey(shortName) ? CredentialsProvider.NONE : r); providerByName.put(simpleName, providerByName.containsKey(simpleName) ? CredentialsProvider.NONE : r); } } for (Iterator<CredentialsProvider> iterator = providerByName.values().iterator(); iterator.hasNext();) { CredentialsProvider p = iterator.next(); if (p == CredentialsProvider.NONE) { iterator.remove(); } } return providerByName; } /** * Value class to hold the details of a {@link CredentialsStore}. * * @since 2.0 */ @Restricted(NoExternalUse.class) public static final class StoreItem implements IconSpec, ModelObject { /** * The store. */ private final CredentialsStore store; /** * The URL we will expose the store at. */ private final String url; /** * Constructor. * * @param store the store. */ public StoreItem(CredentialsStore store) { this.store = store; String provider = store.getProvider().getClass().getName(); String resolver = null; String token = null; ModelObject storeContext = store.getContext(); // we only support the cases where the for (ContextResolver r : ExtensionList.lookup(ContextResolver.class)) { String t = r.getToken(storeContext); if (t != null) { resolver = r.getClass().getName(); token = t; break; } } this.url = token == null ? null : String.format("descriptor/%s/resolver/%s/provider/%s/context/%s", CredentialsSelectHelper.class.getName(), Util.rawEncode(resolver), Util.rawEncode(provider), Util.rawEncode(token)); } /** * {@inheritDoc} */ @Override public String getIconClassName() { return store.getProvider().getIconClassName(); } /** * Exposes if this store is enabled for the current user. * * @return {@code true} if the current user can add credentials to this store. */ public boolean isEnabled() { return url != null && store.hasPermission(CREATE) && !store.getCredentialsDescriptors().isEmpty(); } /** * {@inheritDoc} */ @Override public String getDisplayName() { return store.getContextDisplayName(); } /** * Exposes the description of this store (i.e. the {@link CredentialsProvider#getDisplayName()}. * * @return the description of this store (i.e. the {@link CredentialsProvider#getDisplayName()}. */ public String getDescription() { return store.getProvider().getDisplayName(); } /** * Exposes the URL of this store's {@link WrappedCredentialsStore}. * * @return the URL of this store's {@link WrappedCredentialsStore} */ public String getUrl() { return url; } } /** * Stapler binding for {@link ContextResolver}. * * @since 2.0 */ @Restricted(NoExternalUse.class) public static final class WrappedContextResolver { /** * Our {@link ContextResolver} */ @NonNull private final ContextResolver resolver; /** * Our constructor. * * @param resolver the {@link ContextResolver} */ public WrappedContextResolver(@NonNull ContextResolver resolver) { this.resolver = resolver; } /** * Stapler web binding for the {@link CredentialsProvider}. * * @param className the class name of the {@link CredentialsProvider}. * @return the {@link WrappedContextResolverCredentialsProvider} or {@code null} */ @CheckForNull public WrappedContextResolverCredentialsProvider getProvider(String className) { for (CredentialsProvider p : CredentialsProvider.enabled()) { if (p.getClass().getName().equals(className)) { return new WrappedContextResolverCredentialsProvider(resolver, p); } } return null; } } /** * Stapler binding for a {@link ContextResolver} and {@link CredentialsProvider}. * * @since 2.0 */ @Restricted(NoExternalUse.class) public static final class WrappedContextResolverCredentialsProvider { /** * Our {@link ContextResolver} */ @NonNull private final ContextResolver resolver; /** * Our {@link CredentialsProvider} */ @NonNull private final CredentialsProvider provider; /** * Our constructor. * * @param resolver the {@link ContextResolver} * @param provider the {@link CredentialsProvider} */ public WrappedContextResolverCredentialsProvider(@NonNull ContextResolver resolver, @NonNull CredentialsProvider provider) { this.resolver = resolver; this.provider = provider; } /** * Stapler web binding for the {@link ModelObject} representing the context of the store. * * @param token the {@link ContextResolver#getToken(ModelObject)} of the context of the store. * @return the {@link WrappedContextResolverCredentialsProvider} or {@code null} */ public WrappedCredentialsStore getContext(String token) { ModelObject context = resolver.getContext(token); if (context != null) { CredentialsStore store = provider.getStore(context); if (store != null) { return new WrappedCredentialsStore(resolver, provider, token, store); } } return null; } } /** * Stapler binding for a {@link CredentialsStore}. * * @since 2.0 */ @Restricted(NoExternalUse.class) public static final class WrappedCredentialsStore implements IconSpec, ModelObject { /** * Our {@link ContextResolver} */ @NonNull private final ContextResolver resolver; /** * Our {@link CredentialsProvider} */ @NonNull private final CredentialsProvider provider; /** * Our context's {@link ContextResolver#getToken(ModelObject)}. */ @NonNull private final String token; /** * Our {@link CredentialsStore}. */ private final CredentialsStore store; /** * Our constructor. * * @param resolver the {@link ContextResolver} * @param provider the {@link CredentialsProvider} * @param token the context's {@link ContextResolver#getToken(ModelObject)}. * @param store the {@link CredentialsStore} */ public WrappedCredentialsStore(ContextResolver resolver, CredentialsProvider provider, String token, CredentialsStore store) { this.store = store; this.resolver = resolver; this.provider = provider; this.token = token; } /** * Stapler web binding for adding credentials to the domain. * * @param req the request. * @param rsp the response. * @throws IOException if something goes wrong. * @throws ServletException if something goes wrong. */ @RequirePOST public void doAddCredentials(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { if (!store.isDomainsModifiable()) { hudson.util.HttpResponses.status(400).generateResponse(req, rsp, null); FormApply.applyResponse("window.alert('Domain is read-only')").generateResponse(req, rsp, null); } store.checkPermission(CredentialsStoreAction.CREATE); JSONObject data = req.getSubmittedForm(); String domainName = data.getString("domain"); CredentialsStoreAction.DomainWrapper wrapper = getWrappers().get(domainName); if (!store.getDomains().contains(wrapper.getDomain())) { hudson.util.HttpResponses.status(400).generateResponse(req, rsp, null); FormApply.applyResponse("window.alert('Store does not have selected domain')").generateResponse(req, rsp, null); } store.checkPermission(CredentialsStoreAction.CREATE); Credentials credentials = req.bindJSON(Credentials.class, data.getJSONObject("credentials")); store.addCredentials(wrapper.getDomain(), credentials); FormApply.applyResponse("window.credentials.refreshAll();").generateResponse(req, rsp, null); } /** * Returns a {@link CredentialsStoreAction.DomainWrapper} to use for contextualizing the credentials form. * * @return a {@link CredentialsStoreAction.DomainWrapper} to use for contextualizing the credentials form. */ public CredentialsStoreAction.DomainWrapper getWrapper() { Collection<CredentialsStoreAction.DomainWrapper> values = getWrappers().values(); return values.isEmpty() ? null : values.iterator().next(); } /** * {@inheritDoc} */ @Override public String getIconClassName() { return store.getProvider().getIconClassName(); } /** * {@inheritDoc} */ @Override public String getDisplayName() { return store.getContextDisplayName(); } /** * Exposes the description of this store (i.e. the {@link CredentialsProvider#getDisplayName()}. * * @return the description of this store (i.e. the {@link CredentialsProvider#getDisplayName()}. */ public String getDescription() { return store.getProvider().getDisplayName(); } /** * Exposes our URL (as we will be invoked from an unknown page so we need an absolute URL). * * @return our URL. */ public String getUrl() { return String.format("%sdescriptor/%s/resolver/%s/provider/%s/context/%s", Jenkins.getActiveInstance().getRootUrlFromRequest(), CredentialsSelectHelper.class.getName(), Util.rawEncode(resolver.getClass().getName()), Util.rawEncode(provider.getClass().getName()), Util.rawEncode(token)); } /** * Exposes the {@link CredentialsDescriptor} instances appropriate for this {@link CredentialsStore}. * * @return the {@link CredentialsDescriptor} instances appropriate for this {@link CredentialsStore}. */ public List<CredentialsDescriptor> getCredentialsDescriptors() { return store.getCredentialsDescriptors(); } /** * The {@link CredentialsStoreAction.DomainWrapper} instances. * * @return the {@link CredentialsStoreAction.DomainWrapper} instances. */ public Map<String, CredentialsStoreAction.DomainWrapper> getWrappers() { CredentialsStoreAction action = store.getStoreAction(); return action != null ? action.getDomains() : new CredentialsStoreAction() { /** * {@inheritDoc} */ @NonNull @Override public CredentialsStore getStore() { return store; } }.getDomains(); } /** * The backing {@link CredentialsStore}. * @return backing {@link CredentialsStore}. * @since 2.1.5 */ public CredentialsStore getStore() { return store; } } /** * An extension point to allow plugging in additional resolution of {@link ModelObject} instances. * * @since 2.0 */ public static abstract class ContextResolver implements ExtensionPoint { /** * A {@link ContextResolver} that can be used as a marker. * * @since 2.1.1 */ public static final ContextResolver NONE = new ContextResolver() { /** * {@inheritDoc} */ @Override public String getToken(ModelObject context) { return null; } /** * {@inheritDoc} */ @Override public ModelObject getContext(String token) { return null; } /** * {@inheritDoc} */ @Override public String getDisplayName() { return "Nothing"; } }; /** * Maps a context object (a {@link ModelObject}) into a token that can be used to recover the context. * * @param context the {@link ModelObject}. * @return a token if this {@link ContextResolver} can recover the object or {@code null} if the * {@link ModelObject} type is not supported by this {@link ContextResolver}. */ @CheckForNull public abstract String getToken(ModelObject context); /** * Maps a token from {@link #getToken(ModelObject)} back to its original {@link ModelObject}. * * @param token the token. * @return the corresponding {@link ModelObject} or {@code null} if the object no longer exists or if the * user does not have permission to access that object. */ @CheckForNull public abstract ModelObject getContext(String token); /** * Returns a human readable descripton of the type of context objects that this resolver resolves. * @return a human readable descripton of the type of context objects that this resolver resolves. * @throws AbstractMethodError if somebody compiled against pre-2.1.1 implementations. Use * {@link CredentialsSelectHelper.ContextResolver#displayName(CredentialsSelectHelper.ContextResolver)} * if you would prefer not to have to catch them. * @since 2.1.1 */ @NonNull public abstract String getDisplayName(); /** * Returns a human readable descripton of the type of context objects that the specified resolver resolves. * * @param resolver the context resolver to get the display name of. * @return a human readable descripton of the type of context objects that the specified resolver resolves. * @since 2.1.1 */ public static String displayName(ContextResolver resolver) { try { return resolver.getDisplayName(); } catch (AbstractMethodError e) { // should not get here as do not anticipate new implementations that cannot target 2.1.1 as a baseline return resolver.getClass().getName(); } } } /** * A {@link ContextResolver} for {@link Jenkins}. * * @since 2.0 */ @Extension(ordinal = 1000) public static class SystemContextResolver extends ContextResolver { /** * {@inheritDoc} */ @Override public String getToken(ModelObject context) { return context instanceof Jenkins ? "jenkins" : null; } /** * {@inheritDoc} */ @Override public ModelObject getContext(String token) { return "jenkins".equals(token) ? Jenkins.getActiveInstance() : null; } /** * {@inheritDoc} */ @NonNull @Override public String getDisplayName() { return "Jenkins"; } } /** * A {@link ContextResolver} for {@link Item} instances resolvable by {@link Jenkins#getItemByFullName(String)}. * * @since 2.0 */ @Extension public static class ItemContextResolver extends ContextResolver { /** * {@inheritDoc} */ @Override public String getToken(ModelObject context) { return context instanceof Item ? ((Item) context).getFullName() : null; } /** * {@inheritDoc} */ @Override public ModelObject getContext(String token) { return Jenkins.getActiveInstance().getItemByFullName(token); } /** * {@inheritDoc} */ @NonNull @Override public String getDisplayName() { return "Items"; } } /** * A {@link ContextResolver} for {@link User} instances. * * @since 2.0 */ @Extension public static class UserContextResolver extends ContextResolver { /** * {@inheritDoc} */ @Override public String getToken(ModelObject context) { return context instanceof User ? ((User) context).getId() : null; } /** * {@inheritDoc} */ @Override public ModelObject getContext(String token) { // TODO invoke User.getById directly once Jenkins 2.3+ try { Method getById = User.class.getMethod("getById", String.class, boolean.class); return (ModelObject) getById.invoke(null, token, false); } catch (NoSuchMethodException e) { // old Jenkins pre SECURITY-243 return User.get(token, false, Collections.emptyMap()); } catch (InvocationTargetException e) { return null; } catch (IllegalAccessException e) { return null; } } /** * {@inheritDoc} */ @NonNull @Override public String getDisplayName() { return "Users"; } } }