Java tutorial
/* * The MIT License * * Copyright (c) 2013-2016, CloudBees, Inc.. * * 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 com.cloudbees.plugins.credentials.common.IdCredentials; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.DomainSpecification; import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.XppDriver; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.DescriptorExtensionList; import hudson.Extension; import hudson.Util; import hudson.model.AbstractDescribableImpl; import hudson.model.Action; import hudson.model.Api; import hudson.model.Descriptor; import hudson.model.Failure; import hudson.model.Fingerprint; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.Items; import hudson.model.ModelObject; import hudson.model.User; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.util.FormValidation; import hudson.util.HttpResponses; import hudson.util.Secret; import hudson.util.XStream2; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringReader; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; import javax.annotation.Nonnull; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu; import jenkins.util.xml.XMLUtils; import net.sf.json.JSONObject; import org.acegisecurity.AccessDeniedException; import org.apache.commons.codec.binary.Base64; 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.stapler.AncestorInPath; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.WebMethod; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; import org.xml.sax.SAXException; import static com.cloudbees.plugins.credentials.ContextMenuIconUtils.getMenuItemIconUrlByClassSpec; /** * An action for a {@link CredentialsStore} */ @ExportedBean public abstract class CredentialsStoreAction implements Action, IconSpec, AccessControlled, ModelObjectWithContextMenu, ModelObjectWithChildren { /** * Expose {@link CredentialsProvider#VIEW} for Jelly. */ public static final Permission VIEW = CredentialsProvider.VIEW; /** * Expose {@link CredentialsProvider#CREATE} for Jelly. */ public static final Permission CREATE = CredentialsProvider.CREATE; /** * Expose {@link CredentialsProvider#UPDATE} for Jelly. */ public static final Permission UPDATE = CredentialsProvider.UPDATE; /** * Expose {@link CredentialsProvider#DELETE} for Jelly. */ public static final Permission DELETE = CredentialsProvider.DELETE; /** * Expose {@link CredentialsProvider#MANAGE_DOMAINS} for Jelly. */ public static final Permission MANAGE_DOMAINS = CredentialsProvider.MANAGE_DOMAINS; /** * An {@link XStream2} that replaces {@link Secret} and {@link SecretBytes} instances with {@code <secret-redacted/>} * * @since 2.1.1 */ public static final XStream2 SECRETS_REDACTED; /** * An {@link XStream2} that replaces {@link Secret} and {@link SecretBytes} instances with a hash of the secret and * omits fields that should be excluded from credentials fingerprinting. * * @since 2.1.15 */ public static final XStream2 FINGERPRINT_XML; static { SECRETS_REDACTED = new XStream2(); SECRETS_REDACTED.registerConverter(new Converter() { /** * {@inheritDoc} */ public boolean canConvert(Class type) { return type == Secret.class || type == SecretBytes.class; } /** * {@inheritDoc} */ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { writer.startNode("secret-redacted"); writer.endNode(); } /** * {@inheritDoc} */ public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) { return null; } }); FINGERPRINT_XML = new XStream2(); FINGERPRINT_XML.omitField(BaseStandardCredentials.class, "description"); FINGERPRINT_XML.omitField(StandardCredentials.class, "description"); FINGERPRINT_XML.registerConverter(new Converter() { /** * {@inheritDoc} */ public boolean canConvert(Class type) { return type == Secret.class || type == SecretBytes.class; } /** * {@inheritDoc} */ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { writer.startNode("secret-hashed"); try { // we hash the encrypted value to mix the secret data is mixed with the instance encryption // key and make it harder to infer from the final MD5 hash of the whole what either the exact // encrypted value or the encryption key is. MessageDigest digest = MessageDigest.getInstance("SHA-256"); if (source instanceof Secret) { Secret s = (Secret) source; digest.update(s.getEncryptedValue().getBytes(StandardCharsets.US_ASCII)); } else if (source instanceof SecretBytes) { SecretBytes s = (SecretBytes) source; digest.update(s.toString().getBytes(StandardCharsets.US_ASCII)); } writer.setValue(Base64.encodeBase64String(digest.digest())); } catch (NoSuchAlgorithmException e) { // will never happen as JLS mandates SHA-256, but if it does we just don't provide a hash } writer.endNode(); } /** * {@inheritDoc} */ public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) { return null; } }); } /** * Returns the {@link CredentialsStore} backing this action. * * @return the {@link CredentialsStore}. */ @NonNull public abstract CredentialsStore getStore(); /** * {@inheritDoc} */ @Override public String getIconFileName() { return isVisible() ? "/plugin/credentials/images/24x24/credentials.png" : null; } /** * {@inheritDoc} */ @Override public String getDisplayName() { CredentialsStore store = getStore(); if (this == store.getStoreAction()) { Class<?> c = store.getClass(); while (c.getEnclosingClass() != null) { c = c.getEnclosingClass(); } String name = c.getSimpleName().replaceAll("(?i)(Impl|Credentials|Provider|Store)+", ""); if (StringUtils.isBlank(name)) { name = c.getSimpleName(); } return StringUtils.join(StringUtils.splitByCharacterTypeCamelCase(name), ' '); } else { return Messages.CredentialsStoreAction_DisplayName(); } } /** * Any additional actions to display for this {@link CredentialsStore}. * * @return Any additional actions to display for this {@link CredentialsStore}. * @since 2.0 */ @NonNull public List<Action> getActions() { return Collections.emptyList(); } /** * Exposes the {@link #getActions()} for Stapler. * * @param token the name of the action. * @return the {@link Action} or {@code null} * @since 2.0 */ @CheckForNull @SuppressWarnings("unused") // stapler binding public Object getDynamic(String token) { for (Action a : getActions()) { String url = a.getUrlName(); if (url == null) { continue; } if (url.equals(token)) { return a; } } return null; } /** * Creates the context menu with the supplied prefix to all URLs. * * @param prefix the prefix to prepend to relative urls. * @return the {@link ContextMenu} or {@code null} * @since 2.0 */ @CheckForNull public ContextMenu getContextMenu(String prefix) { ContextMenu menu = new ContextMenu(); if (getStore().isDomainsModifiable() && getStore().hasPermission(MANAGE_DOMAINS)) { menu.add(ContextMenuIconUtils.buildUrl(prefix, "newDomain"), getMenuItemIconUrlByClassSpec("icon-credentials-new-domain icon-md"), Messages.CredentialsStoreAction_AddDomainAction()); } for (Action action : getActions()) { ContextMenuIconUtils.addMenuItem(menu, prefix, action); } return menu.items.isEmpty() ? null : menu; } /** * Creates the children context menu with the supplied prefix to all URLs. * * @param prefix the prefix to prepend to relative urls. * @return the {@link ContextMenu} or {@code null} * @since 2.0 */ @CheckForNull public ContextMenu getChildrenContextMenu(String prefix) { ContextMenu menu = new ContextMenu(); for (Domain d : getStore().getDomains()) { MenuItem item = new MenuItem(d.getUrl(), getMenuItemIconUrlByClassSpec("icon-credentials-domain icon-md"), d.isGlobal() ? Messages.CredentialsStoreAction_GlobalDomainDisplayName() : d.getName()); item.subMenu = new DomainWrapper(this, d) .getContextMenu(ContextMenuIconUtils.buildUrl(prefix, d.getUrl())); menu.add(item); } return menu.items.isEmpty() ? null : menu; } /** * {@inheritDoc} */ @Override public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { return getContextMenu(""); } /** * {@inheritDoc} */ @Override public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { return getChildrenContextMenu(""); } /** * {@inheritDoc} */ @Override public String getUrlName() { CredentialsStore store = getStore(); if (this == store.getStoreAction()) { Class<?> c = store.getClass(); while (c.getEnclosingClass() != null) { c = c.getEnclosingClass(); } String name = c.getSimpleName().replaceAll("(?i)(Impl|Credentials|Provider|Store)+", ""); if (StringUtils.isBlank(name)) { name = c.getSimpleName(); } return StringUtils.join(StringUtils.splitByCharacterTypeCamelCase(name), '-') .toLowerCase(Locale.ENGLISH); } else { return "credential-store"; } } /** * Expose the action's {@link Api}. * * @return the action's {@link Api}. */ public Api getApi() { return new Api(this); } /** * Checks if this action should be visible. * * @return {@code true} if the action should be visible. */ public boolean isVisible() { CredentialsStore store = getStore(); if (!store.getProvider().isEnabled()) { return false; } CredentialsStoreAction storeAction = store.getStoreAction(); if (storeAction != null && this != storeAction) { // 2.0+ implementations of CredentialsStore should be returning their action via getStoreAction() // and we want to display that action from ViewCredentialsAction // Old implementations will be returning null from getStoreAction() so we let them display as before // Forward looking implementations written against the old API will want to "hide" their old // action and display the new one returned from getStoreAction() which is what this hook enables. return false; } return store.hasPermission(CredentialsProvider.VIEW) && !store.getCredentialsDescriptors().isEmpty(); } /** * {@inheritDoc} */ @Override public String getIconClassName() { return isVisible() ? "icon-credentials-credentials" : null; } /** * Returns the {@link Item#getFullName()} or nearest approximation. * * @return the {@link Item#getFullName()} or nearest approximation. */ public final String getFullName() { String n; ModelObject context = getStore().getContext(); if (context instanceof Item) { n = ((Item) context).getFullName(); } else if (context instanceof ItemGroup) { n = ((ItemGroup) context).getFullName(); } else if (context instanceof User) { n = "user:" + ((User) context).getId(); } else { n = ""; } if (n.length() == 0) { return getUrlName(); } else { return n + '/' + getUrlName(); } } /** * Returns the {@link Item#getFullDisplayName()} or nearest approximation. * * @return the {@link Item#getFullDisplayName()} or nearest approximation. */ public final String getFullDisplayName() { String n; ModelObject context = getStore().getContext(); if (context instanceof Item) { n = ((Item) context).getFullDisplayName(); } else if (context instanceof ItemGroup) { n = ((ItemGroup) context).getFullDisplayName(); } else if (context instanceof User) { n = Messages.CredentialsStoreAction_UserDisplayName(((User) context).getDisplayName()); } else { // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline n = Jenkins.getActiveInstance().getFullDisplayName(); } if (n.length() == 0) { return getDisplayName(); } else { return n + " \u00BB " + getDisplayName(); } } /** * Returns the map of {@link DomainWrapper} instances. * * @return the map of {@link DomainWrapper} instances. */ @Exported @NonNull public Map<String, DomainWrapper> getDomains() { Map<String, DomainWrapper> result = new TreeMap<String, DomainWrapper>(); for (Domain d : getStore().getDomains()) { String name; if (d.isGlobal()) { name = "_"; } else { name = d.getName(); } result.put(name, new DomainWrapper(this, d)); } return result; } /** * Gets the named {@link DomainWrapper}. * * @param name the name. * @return the named {@link DomainWrapper}. */ @CheckForNull public DomainWrapper getDomain(String name) { return getDomains().get(name); } /** * Exposes {@link CredentialsStore#isDomainsModifiable()} for Jelly. * * @return {@link CredentialsStore#isDomainsModifiable()}. */ public boolean isDomainsModifiable() { return getStore().isDomainsModifiable(); } /** * Exposes {@link DomainWrapper.DescriptorImpl} for Jelly. * * @return {@link DomainWrapper.DescriptorImpl}. */ public DomainWrapper.DescriptorImpl getDomainDescriptor() { // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline return Jenkins.getActiveInstance().getDescriptorByType(DomainWrapper.DescriptorImpl.class); } /** * Gets all the {@link com.cloudbees.plugins.credentials.domains.DomainSpecification} descriptors. * * @return all the {@link com.cloudbees.plugins.credentials.domains.DomainSpecification} descriptors. */ @SuppressWarnings("unused") // used by stapler public DescriptorExtensionList<DomainSpecification, Descriptor<DomainSpecification>> getSpecificationDescriptors() { // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline return Jenkins.getActiveInstance().getDescriptorList(DomainSpecification.class); } /** * Creates a domain. * * @param req the request. * @return the response. * @throws ServletException if something goes wrong. * @throws IOException if something goes wrong. */ @SuppressWarnings("unused") // stapler web method @Restricted(NoExternalUse.class) @RequirePOST public HttpResponse doCreateDomain(StaplerRequest req) throws ServletException, IOException { getStore().checkPermission(MANAGE_DOMAINS); if (!getStore().isDomainsModifiable()) { return HttpResponses.status(HttpServletResponse.SC_BAD_REQUEST); } String requestContentType = req.getContentType(); if (requestContentType == null) { throw new Failure("No Content-Type header set"); } if (requestContentType.startsWith("application/xml") || requestContentType.startsWith("text/xml")) { final StringWriter out = new StringWriter(); try { XMLUtils.safeTransform(new StreamSource(req.getReader()), new StreamResult(out)); out.close(); } catch (TransformerException e) { throw new IOException("Failed to parse credential", e); } catch (SAXException e) { throw new IOException("Failed to parse credential", e); } Domain domain = (Domain) Items.XSTREAM .unmarshal(new XppDriver().createReader(new StringReader(out.toString()))); if (getStore().addDomain(domain)) { return HttpResponses.ok(); } else { return HttpResponses.status(HttpServletResponse.SC_CONFLICT); } } else { JSONObject data = req.getSubmittedForm(); Domain domain = req.bindJSON(Domain.class, data); String domainName = domain.getName(); if (domainName != null && getStore().addDomain(domain)) { return HttpResponses.redirectTo("./domain/" + Util.rawEncode(domainName)); } return HttpResponses.redirectToDot(); } } /** * {@inheritDoc} */ @Nonnull @Override public ACL getACL() { return getStore().getACL(); } /** * {@inheritDoc} */ @Override public void checkPermission(@Nonnull Permission permission) throws AccessDeniedException { getACL().checkPermission(permission); } /** * {@inheritDoc} */ @Override public boolean hasPermission(@Nonnull Permission permission) { return getACL().hasPermission(permission); } /** * A wrapper object to bind and expose {@link Domain} instances into the web UI. */ @ExportedBean public static class DomainWrapper extends AbstractDescribableImpl<DomainWrapper> implements ModelObjectWithContextMenu, ModelObjectWithChildren, AccessControlled { /** * The {@link CredentialsStoreAction} that we belong to. */ private final CredentialsStoreAction parent; /** * The {@link Domain} that we are exposing. */ private final Domain domain; /** * Our constructor. * * @param parent our parent action. * @param domain the domain we are wrapping. */ public DomainWrapper(CredentialsStoreAction parent, Domain domain) { this.parent = parent; this.domain = domain; } /** * Expose a Jenkins {@link Api}. * * @return the {@link Api}. */ public Api getApi() { return new Api(this); } /** * Expose the backing {@link CredentialsStore}. * * @return the backing {@link CredentialsStore}. */ public CredentialsStore getStore() { return getParent().getStore(); } /** * Expose the backing {@link Domain}. * * @return the backing {@link Domain}. */ public Domain getDomain() { return domain; } /** * Expose the parent {@link CredentialsStoreAction}. * * @return the parent {@link CredentialsStoreAction}. */ public CredentialsStoreAction getParent() { return parent; } /** * Return the URL name. * * @return the URL name. */ @Exported @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "isGlobal() check implies that domain.getName() is null") public String getUrlName() { return isGlobal() ? "_" : Util.rawEncode(domain.getName()); } /** * Return the display name. * * @return the display name. */ @Exported public String getDisplayName() { return isGlobal() ? Messages.CredentialsStoreAction_GlobalDomainDisplayName() : domain.getName(); } /** * Return the full name. * * @return the full name. */ @Exported public final String getFullName() { String n = getParent().getFullName(); if (n.length() == 0) { return getUrlName(); } else { return n + '/' + getUrlName(); } } /** * Return the full display name. * * @return the full display name. */ @Exported public final String getFullDisplayName() { String n = getParent().getFullDisplayName(); if (n.length() == 0) { return getDisplayName(); } else { return n + " \u00BB " + getDisplayName(); } } /** * Expose the {@link Domain#getDescription()}. * * @return the {@link Domain#getDescription()}. */ @Exported public String getDescription() { return isGlobal() ? Messages.CredentialsStoreAction_GlobalDomainDescription() : domain.getDescription(); } /** * Expose a flag to indicate that the wrapped domain is the global domain. * * @return {@code true} if and only if the wrapped domain is the global domain. */ @Exported public boolean isGlobal() { return domain == Domain.global(); } /** * Expose {@link CredentialsWrapper.DescriptorImpl} to Jelly. * * @return the {@link CredentialsWrapper.DescriptorImpl} singleton. */ public CredentialsWrapper.DescriptorImpl getCredentialDescriptor() { // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline return Jenkins.getActiveInstance().getDescriptorByType(CredentialsWrapper.DescriptorImpl.class); } /** * Exposes a map of the wrapped credentials. * * @return a map of the wrapped credentials. */ @NonNull public Map<String, CredentialsWrapper> getCredentials() { Map<String, CredentialsWrapper> result = new LinkedHashMap<String, CredentialsWrapper>(); int index = 0; for (Credentials c : getStore().getCredentials(domain)) { String id; if (c instanceof IdCredentials) { id = ((IdCredentials) c).getId(); } else { while (result.containsKey("index-" + index)) { index++; } id = "index-" + index; index++; } result.put(id, new CredentialsWrapper(this, c, id)); } return result; } /** * Exposes the wrapped credentials for the XML API. * * @return the wrapped credentials for the XML API. * @since 2.1.0 */ @NonNull @Exported(name = "credentials", visibility = 1) public List<CredentialsWrapper> getCredentialsList() { return new ArrayList<CredentialsWrapper>(getCredentials().values()); } /** * Get a credential by id. * * @param id the id. * @return the {@link CredentialsWrapper}. */ @CheckForNull public CredentialsWrapper getCredential(String id) { return getCredentials().get(id); } /** * Creates a credential. * * @param req the request. * @return the response. * @throws ServletException if something goes wrong. * @throws IOException if something goes wrong. */ @RequirePOST @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public HttpResponse doCreateCredentials(StaplerRequest req) throws ServletException, IOException { getStore().checkPermission(CREATE); String requestContentType = req.getContentType(); if (requestContentType == null) { throw new Failure("No Content-Type header set"); } if (requestContentType.startsWith("application/xml") || requestContentType.startsWith("text/xml")) { final StringWriter out = new StringWriter(); try { XMLUtils.safeTransform(new StreamSource(req.getReader()), new StreamResult(out)); out.close(); } catch (TransformerException e) { throw new IOException("Failed to parse credential", e); } catch (SAXException e) { throw new IOException("Failed to parse credential", e); } Credentials credentials = (Credentials) Items.XSTREAM .unmarshal(new XppDriver().createReader(new StringReader(out.toString()))); if (getStore().addCredentials(domain, credentials)) { return HttpResponses.ok(); } else { return HttpResponses.status(HttpServletResponse.SC_CONFLICT); } } else { JSONObject data = req.getSubmittedForm(); Credentials credentials = req.bindJSON(Credentials.class, data.getJSONObject("credentials")); getStore().addCredentials(domain, credentials); return HttpResponses.redirectTo("../../domain/" + getUrlName()); } } /** * Updates the domain configuration. * * @param req the request. * @return the response. * @throws ServletException if something goes wrong. * @throws IOException if something goes wrong. */ @RequirePOST @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public HttpResponse doConfigSubmit(StaplerRequest req) throws ServletException, IOException { if (!getStore().isDomainsModifiable()) { return HttpResponses.status(400); } getStore().checkPermission(MANAGE_DOMAINS); JSONObject data = req.getSubmittedForm(); Domain domain = req.bindJSON(Domain.class, data); String domainName = domain.getName(); if (domainName != null && getStore().updateDomain(this.domain, domain)) { return HttpResponses.redirectTo("../../domain/" + Util.rawEncode(domainName)); } return HttpResponses.redirectToDot(); } /** * Deletes a domain. * * @param req the request. * @return the response. * @throws IOException if something goes wrong. */ @RequirePOST @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public HttpResponse doDoDelete(StaplerRequest req) throws IOException { if (!getStore().isDomainsModifiable()) { return HttpResponses.status(400); } getStore().checkPermission(MANAGE_DOMAINS); if (getStore().removeDomain(domain)) { return HttpResponses.redirectTo("../.."); } return HttpResponses.redirectToDot(); } /** * Creates the context menu with the supplied prefix to all URLs. * * @param prefix the prefix to prepend to relative urls. * @return the {@link ContextMenu} or {@code null} * @since 2.0 */ @CheckForNull public ContextMenu getContextMenu(String prefix) { if (getStore().hasPermission(CREATE) || (getStore().hasPermission(MANAGE_DOMAINS) && !domain.isGlobal())) { ContextMenu result = new ContextMenu(); if (getStore().hasPermission(CREATE)) { result.add(new MenuItem(ContextMenuIconUtils.buildUrl(prefix, "newCredentials"), getMenuItemIconUrlByClassSpec("icon-credentials-new-credential icon-md"), Messages.CredentialsStoreAction_AddCredentialsAction())); } if (getStore().hasPermission(MANAGE_DOMAINS) && !domain.isGlobal()) { result.add(new MenuItem(ContextMenuIconUtils.buildUrl(prefix, "configure"), getMenuItemIconUrlByClassSpec("icon-setting icon-md"), Messages.CredentialsStoreAction_ConfigureDomainAction())); result.add(new MenuItem(ContextMenuIconUtils.buildUrl(prefix, "delete"), getMenuItemIconUrlByClassSpec("icon-edit-delete icon-md"), Messages.CredentialsStoreAction_DeleteDomainAction())); } return result.items.isEmpty() ? null : result; } return null; } /** * Creates the children context menu with the supplied prefix to all URLs. * * @param prefix the prefix to prepend to relative urls. * @return the {@link ContextMenu} or {@code null} * @since 2.0 */ @CheckForNull public ContextMenu getChildrenContextMenu(String prefix) { ContextMenu menu = new ContextMenu(); for (Map.Entry<String, CredentialsWrapper> entry : getCredentials().entrySet()) { String p = ContextMenuIconUtils.buildUrl(prefix, "credential", entry.getKey()); MenuItem item = new MenuItem(p, getMenuItemIconUrlByClassSpec(entry.getValue().getIconClassName() + " icon-md"), entry.getValue().getDisplayName()); item.subMenu = entry.getValue().getContextMenu(p); menu.add(item); } return menu.items.isEmpty() ? null : menu; } /** * {@inheritDoc} */ @Override public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { return getContextMenu(""); } /** * {@inheritDoc} */ @Override public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { return getChildrenContextMenu(""); } /** * Accepts {@literal config.xml} submission, as well as serve it. * * @param req the request * @param rsp the response * @throws IOException if things go wrong * @since 2.1.1 */ @WebMethod(name = "config.xml") @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp) throws IOException { getStore().checkPermission(CredentialsProvider.MANAGE_DOMAINS); if (req.getMethod().equals("GET")) { // read rsp.setContentType("application/xml"); Items.XSTREAM2.toXML(domain, new OutputStreamWriter(rsp.getOutputStream(), rsp.getCharacterEncoding())); return; } if (req.getMethod().equals("POST") && getStore().isDomainsModifiable()) { // submission updateByXml(new StreamSource(req.getReader())); return; } if (req.getMethod().equals("DELETE") && getStore().isDomainsModifiable()) { if (getStore().removeDomain(domain)) { return; } else { rsp.sendError(HttpServletResponse.SC_CONFLICT); return; } } // huh? rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); } /** * Updates a {@link Credentials} by its XML definition. * * @param source source of the Item's new definition. * The source should be either a <code>StreamSource</code> or a <code>SAXSource</code>, other * sources may not be handled. * @throws IOException if things go wrong. * @since 2.1.1 */ @Restricted(NoExternalUse.class) public void updateByXml(Source source) throws IOException { getStore().checkPermission(CredentialsProvider.MANAGE_DOMAINS); final StringWriter out = new StringWriter(); try { XMLUtils.safeTransform(source, new StreamResult(out)); out.close(); } catch (TransformerException e) { throw new IOException("Failed to parse credential", e); } catch (SAXException e) { throw new IOException("Failed to parse credential", e); } Domain replacement = (Domain) Items.XSTREAM .unmarshal(new XppDriver().createReader(new StringReader(out.toString()))); getStore().updateDomain(domain, replacement); } /** * {@inheritDoc} */ @Nonnull @Override public ACL getACL() { return getParent().getACL(); } /** * {@inheritDoc} */ @Override public void checkPermission(@Nonnull Permission permission) throws AccessDeniedException { getACL().checkPermission(permission); } /** * {@inheritDoc} */ @Override public boolean hasPermission(@Nonnull Permission permission) { return getACL().hasPermission(permission); } /** * Our Descriptor. */ @Extension public static class DescriptorImpl extends Descriptor<DomainWrapper> { /** * Default constructor. */ public DescriptorImpl() { super(DomainWrapper.class); } /** * {@inheritDoc} */ @Override public String getDisplayName() { return "Domain"; } /** * Form validation for creating a new domain / renaming an existing domain. * * @param wrapper the existing domain or {@code null} * @param action the {@link CredentialsStoreAction} in the request. * @param value the proposed name. * @return the {@link FormValidation} */ @SuppressWarnings("unused") // stapler form validation @Restricted(NoExternalUse.class) public FormValidation doCheckName(@AncestorInPath DomainWrapper wrapper, @AncestorInPath CredentialsStoreAction action, @QueryParameter String value) { if (StringUtils.isBlank(value)) { return FormValidation.error(Messages.CredentialsStoreAction_EmptyDomainNameMessage()); } try { Jenkins.checkGoodName(value); } catch (Failure e) { return FormValidation.error(e.getMessage()); } if (action != null) { for (Domain d : action.getStore().getDomains()) { if (wrapper != null && wrapper.domain == d) { continue; } if (value.equals(d.getName())) { return FormValidation .error(Messages.CredentialsStoreAction_DuplicateDomainNameMessage()); } } } return FormValidation.ok(); } } } /** * A wrapper object to bind and expose {@link Credentials} instances into the web UI. */ @ExportedBean public static class CredentialsWrapper extends AbstractDescribableImpl<CredentialsWrapper> implements IconSpec, ModelObjectWithContextMenu, AccessControlled { /** * Our {@link DomainWrapper}. */ private final DomainWrapper domain; /** * The {@link Credentials} that we are wrapping. */ private final Credentials credentials; /** * The {@link IdCredentials#getId()} of the {@link Credentials}. */ private final String id; private Fingerprint fingerprint; /** * Constructor. * * @param domain the wrapped domain. * @param credentials the credentials. * @param id the id. */ public CredentialsWrapper(DomainWrapper domain, Credentials credentials, String id) { this.domain = domain; this.credentials = credentials; this.id = id; } /** * Return the id for the XML API. * * @return the id. * @since 2.1.0 */ @Exported public String getId() { return id; } /** * Return the URL name. * * @return the URL name. */ public String getUrlName() { return Util.rawEncode(id); } /** * {@inheritDoc} */ @Override public String getIconClassName() { return credentials.getDescriptor().getIconClassName(); } /** * Expose a Jenkins {@link Api}. * * @return the {@link Api}. */ public Api getApi() { return new Api(this); } /** * Gets the display name of the {@link Credentials}. * * @return the display name of the {@link Credentials}. */ @Exported public String getDisplayName() { return CredentialsNameProvider.name(credentials); } /** * Gets the display name of the {@link CredentialsDescriptor}. * * @return the display name of the {@link CredentialsDescriptor}. */ @Exported public String getTypeName() { return credentials.getDescriptor().getDisplayName(); } /** * Gets the description of the {@link Credentials}. * * @return the description of the {@link Credentials}. */ @Exported public String getDescription() { return credentials instanceof StandardCredentials ? ((StandardCredentials) credentials).getDescription() : null; } /** * Gets the full name of the {@link Credentials}. * * @return the full name of the {@link Credentials}. */ @Exported public final String getFullName() { String n = getDomain().getFullName(); if (n.length() == 0) { return getUrlName(); } else { return n + '/' + getUrlName(); } } /** * Gets the full display name of the {@link Credentials}. * * @return the full display name of the {@link Credentials}. */ public final String getFullDisplayName() { String n = getDomain().getFullDisplayName(); if (n.length() == 0) { return getDisplayName(); } else { return n + " \u00BB " + getDisplayName(); } } /** * Exposes the backing {@link Credentials}. * * @return the backing {@link Credentials}. */ public Credentials getCredentials() { return credentials; } /** * Exposes the backing {@link DomainWrapper}. * * @return the backing {@link DomainWrapper}. */ public DomainWrapper getDomain() { return domain; } /** * Exposes the backing {@link DomainWrapper}. * * @return the backing {@link DomainWrapper}. */ public DomainWrapper getParent() { return domain; } /** * Exposes the backing {@link CredentialsStore}. * * @return the backing {@link CredentialsStore}. */ public CredentialsStore getStore() { return domain.getStore(); } /** * Exposes the fingerprint for Jelly pages. * * @return the {@link Fingerprint}. * @throws IOException if the {@link Fingerprint} could not be retrieved. * @since 2.1.1 */ @Restricted(NoExternalUse.class) @Exported(visibility = 1) public Fingerprint getFingerprint() throws IOException { if (fingerprint == null) { // idempotent write fingerprint = CredentialsProvider.getFingerprintOf(credentials); } return fingerprint; } /** * Deletes the credentials. * * @param req the request. * @return the response. * @throws IOException if something goes wrong. */ @RequirePOST @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public HttpResponse doDoDelete(StaplerRequest req) throws IOException { getStore().checkPermission(DELETE); if (getStore().removeCredentials(domain.getDomain(), credentials)) { return HttpResponses.redirectTo("../.."); } return HttpResponses.redirectToDot(); } /** * Moves the credential. * * @param req the request. * @param destination the destination * @return the response. * @throws IOException if something goes wrong. */ @RequirePOST @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public HttpResponse doDoMove(StaplerRequest req, @QueryParameter String destination) throws IOException { if (getStore().getDomains().size() <= 1) { return HttpResponses.status(400); } // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline Jenkins jenkins = Jenkins.getActiveInstance(); getStore().checkPermission(DELETE); final String splitKey = domain.getParent().getUrlName() + "/"; int split = destination.lastIndexOf(splitKey); if (split == -1) { return HttpResponses.status(400); } String contextName = destination.substring(0, split); String domainName = destination.substring(split + splitKey.length()); ModelObject context = null; if ("".equals(contextName)) { context = jenkins; } else { while (context == null && split > 0) { context = contextName.startsWith("user:") ? User.get(contextName.substring("user:".length(), split - 1), false, Collections.emptyMap()) : jenkins.getItemByFullName(contextName); if (context == null) { split = destination.lastIndexOf(splitKey, split - 1); if (split > 0) { contextName = destination.substring(0, split); domainName = destination.substring(split + splitKey.length()); } } } } if (context == null) { return HttpResponses.status(400); } CredentialsStore destinationStore = null; Domain destinationDomain = null; for (CredentialsStore store : CredentialsProvider.lookupStores(context)) { if (store.getContext() == context) { for (Domain d : store.getDomains()) { if (domainName.equals("_") ? d.getName() == null : domainName.equals(d.getName())) { destinationStore = store; destinationDomain = d; break; } } if (destinationDomain != null) { break; } } } if (destinationDomain == null) { return HttpResponses.status(400); } if (!destinationStore.isDomainsModifiable()) { return HttpResponses.status(400); } destinationStore.checkPermission(CREATE); if (destinationDomain.equals(domain.getDomain())) { return HttpResponses.redirectToDot(); } if (destinationStore.addCredentials(destinationDomain, credentials)) { if (getStore().removeCredentials(domain.getDomain(), credentials)) { return HttpResponses.redirectTo("../.."); } else { destinationStore.removeCredentials(destinationDomain, credentials); } } return HttpResponses.redirectToDot(); } /** * Updates the credentials. * * @param req the request. * @return the response. * @throws ServletException if something goes wrong. * @throws IOException if something goes wrong. */ @RequirePOST @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public HttpResponse doUpdateSubmit(StaplerRequest req) throws ServletException, IOException { getStore().checkPermission(UPDATE); JSONObject data = req.getSubmittedForm(); Credentials credentials = req.bindJSON(Credentials.class, data); if (!getStore().updateCredentials(this.domain.domain, this.credentials, credentials)) { return HttpResponses.redirectTo("concurrentModification"); } return HttpResponses.redirectToDot(); } /** * Creates the context menu with the supplied prefix to all URLs. * * @param prefix the prefix to prepend to relative urls. * @return the {@link ContextMenu} or {@code null} * @since 2.0 */ @CheckForNull @Restricted(NoExternalUse.class) public ContextMenu getContextMenu(String prefix) { if (getStore().hasPermission(UPDATE) || getStore().hasPermission(DELETE)) { ContextMenu result = new ContextMenu(); if (getStore().hasPermission(UPDATE)) { result.add(new MenuItem(ContextMenuIconUtils.buildUrl(prefix, "update"), getMenuItemIconUrlByClassSpec("icon-setting icon-md"), Messages.CredentialsStoreAction_UpdateCredentialAction())); } if (getStore().hasPermission(DELETE)) { result.add(new MenuItem(ContextMenuIconUtils.buildUrl(prefix, "delete"), getMenuItemIconUrlByClassSpec("icon-edit-delete icon-md"), Messages.CredentialsStoreAction_DeleteCredentialAction())); result.add(new MenuItem(ContextMenuIconUtils.buildUrl(prefix, "move"), getMenuItemIconUrlByClassSpec("icon-credentials-move icon-md"), Messages.CredentialsStoreAction_MoveCredentialAction())); } return result.items.isEmpty() ? null : result; } return null; } /** * {@inheritDoc} */ @Override public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { return getContextMenu(""); } /** * Accepts {@literal config.xml} submission, as well as serve it. * * @param req the request * @param rsp the response * @throws IOException if things go wrong * @since 2.1.1 */ @WebMethod(name = "config.xml") @Restricted(NoExternalUse.class) @SuppressWarnings("unused") // stapler web method public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp) throws IOException { if (req.getMethod().equals("GET")) { // read getStore().checkPermission(VIEW); rsp.setContentType("application/xml"); SECRETS_REDACTED.toXML(credentials, new OutputStreamWriter(rsp.getOutputStream(), rsp.getCharacterEncoding())); return; } if (req.getMethod().equals("POST")) { // submission updateByXml(new StreamSource(req.getReader())); return; } if (req.getMethod().equals("DELETE")) { getStore().checkPermission(DELETE); if (getStore().removeCredentials(domain.getDomain(), credentials)) { return; } else { rsp.sendError(HttpServletResponse.SC_CONFLICT); return; } } // huh? rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); } /** * Updates a {@link Credentials} by its XML definition. * * @param source source of the Item's new definition. * The source should be either a <code>StreamSource</code> or a <code>SAXSource</code>, other * sources may not be handled. * @throws IOException if things go wrong * @since 2.1.1 */ @Restricted(NoExternalUse.class) public void updateByXml(Source source) throws IOException { getStore().checkPermission(UPDATE); final StringWriter out = new StringWriter(); try { XMLUtils.safeTransform(source, new StreamResult(out)); out.close(); } catch (TransformerException e) { throw new IOException("Failed to parse credential", e); } catch (SAXException e) { throw new IOException("Failed to parse credential", e); } Credentials credentials = (Credentials) Items.XSTREAM .unmarshal(new XppDriver().createReader(new StringReader(out.toString()))); getStore().updateCredentials(domain.getDomain(), this.credentials, credentials); } /** * {@inheritDoc} */ @Nonnull @Override public ACL getACL() { return getParent().getACL(); } /** * {@inheritDoc} */ @Override public void checkPermission(@Nonnull Permission permission) throws AccessDeniedException { getACL().checkPermission(permission); } /** * {@inheritDoc} */ @Override public boolean hasPermission(@Nonnull Permission permission) { return getACL().hasPermission(permission); } /** * Our {@link Descriptor}. */ @Extension public static class DescriptorImpl extends Descriptor<CredentialsWrapper> { /** * Exposes {@link CredentialsProvider#allCredentialsDescriptors()} to Jelly * * @return {@link CredentialsProvider#allCredentialsDescriptors()} */ @Restricted(NoExternalUse.class) public DescriptorExtensionList<Credentials, CredentialsDescriptor> getCredentialDescriptors() { // TODO delete me return CredentialsProvider.allCredentialsDescriptors(); } /** * {@inheritDoc} */ @Override public String getDisplayName() { return "Credential"; } } } }