Skip to content

Commit

Permalink
[JENKINS-28407] Let's add the APIs
Browse files Browse the repository at this point in the history
- CRUD for credentials (read retains secrets redacted)
- CRUD for credentials domains
  • Loading branch information
stephenc committed Jun 13, 2016
1 parent d8c8132 commit 3497ffc
Show file tree
Hide file tree
Showing 13 changed files with 948 additions and 16 deletions.
Expand Up @@ -27,6 +27,12 @@
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainSpecification;
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;
Expand All @@ -40,14 +46,20 @@
import hudson.model.Failure;
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.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
Expand All @@ -57,9 +69,15 @@
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.lang.StringUtils;
Expand All @@ -71,9 +89,11 @@
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;

Expand Down Expand Up @@ -105,6 +125,32 @@ public abstract class CredentialsStoreAction
*/
public static final Permission MANAGE_DOMAINS = CredentialsProvider.MANAGE_DOMAINS;

/**
* An {@link XStream2} that replaces {@link Secret} instances with {@literal REDACTED}
*
* @since 2.1.1
*/
public static final XStream2 SECRETS_REDACTED;

static {
SECRETS_REDACTED = new XStream2();
SECRETS_REDACTED.registerConverter(new Converter() {

public boolean canConvert(Class type) {
return type == Secret.class;
}

public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
writer.startNode("secret-redacted");
writer.endNode();
}

public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
return null;
}
});
}

/**
* Returns the {@link CredentialsStore} backing this action.
*
Expand Down Expand Up @@ -425,14 +471,42 @@ public DescriptorExtensionList<DomainSpecification, Descriptor<DomainSpecificati
@RequirePOST
public HttpResponse doCreateDomain(StaplerRequest req) throws ServletException, IOException {
getStore().checkPermission(MANAGE_DOMAINS);
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));
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();
}
return HttpResponses.redirectToDot();
}

/**
Expand Down Expand Up @@ -666,10 +740,35 @@ public CredentialsWrapper getCredential(String id) {
@SuppressWarnings("unused") // stapler web method
public HttpResponse doCreateCredentials(StaplerRequest req) throws ServletException, IOException {
getStore().checkPermission(CREATE);
JSONObject data = req.getSubmittedForm();
Credentials credentials = req.bindJSON(Credentials.class, data.getJSONObject("credentials"));
getStore().addCredentials(domain, credentials);
return HttpResponses.redirectTo("../../domain/" + getUrlName());
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());
}
}

/**
Expand Down Expand Up @@ -793,6 +892,61 @@ public ContextMenu doChildrenContextMenu(StaplerRequest request,
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")
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;
}

// 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
*/
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);
}

/**
* Our Descriptor.
*/
Expand Down Expand Up @@ -1184,6 +1338,61 @@ public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse respons
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")
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;
}

// 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
*/
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);
}

/**
* Our {@link Descriptor}.
*/
Expand Down

0 comments on commit 3497ffc

Please sign in to comment.