Skip to content

Commit

Permalink
Merge pull request #247 from kmadel/pr-208
Browse files Browse the repository at this point in the history
Add Credentials Support with credentials select widget [JENKINS-35503]
  • Loading branch information
kmadel committed Oct 26, 2016
2 parents e568a98 + 4055f6f commit 40a0ac4
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 65 deletions.
34 changes: 22 additions & 12 deletions README.md
@@ -1,25 +1,35 @@
# Slack plugin for Jenkins - [![Build Status][jenkins-status]][jenkins-builds] [![Slack Signup][slack-badge]][slack-signup]
Slack plugin for Jenkins [![Build Status][jenkins-status]][jenkins-builds] [![Slack Signup][slack-badge]][slack-signup]
----------------------------------------------------------------

Started with a fork of the HipChat plugin:
Provides Jenkins notification integration with Slack.

https://github.com/jlewallen/jenkins-hipchat-plugin
## Install Instructions

1. Get a Slack account: https://slack.com/
2. Configure the Jenkins integration: https://my.slack.com/services/new/jenkins-ci
3. Install this plugin on your Jenkins server
4. Configure it in your Jenkins job (and optionally as global configuration) and **add it as a Post-build action**.

#### Security

Use Jenkins Credentials and a credential ID to configure the Slack integration token. It is a security risk to expose your integration token using the previous *Integration Token* setting.

Create a new ***Secret text*** credential:
![image](https://cloud.githubusercontent.com/assets/983526/17971588/6c26dfa0-6aa9-11e6-808c-3e139446e013.png)

Which was, in turn, a fork of the Campfire plugin.

Select that credential as the value for the ***Integration Token Credential ID*** field:
![image](https://cloud.githubusercontent.com/assets/983526/17971458/ec296bf6-6aa8-11e6-8d19-06d9f1c9d611.png)

#### Jenkins Pipeline Support

Includes [Jenkins Pipeline](https://github.com/jenkinsci/workflow-plugin) support as of version 2.0:

```
slackSend color: 'good', message: 'Message from Jenkins Pipeline'
```

# Jenkins Instructions

1. Get a Slack account: https://slack.com/
2. Configure the Jenkins integration: https://my.slack.com/services/new/jenkins-ci
3. Install this plugin on your Jenkins server
4. Configure it in your Jenkins job and **add it as a Post-build action**.

# Developer instructions
### Developer instructions

Install Maven and JDK. This was last build with Maven 3.2.5 and OpenJDK
1.7.0\_75 on KUbuntu 14.04.
Expand Down
10 changes: 10 additions & 0 deletions pom.xml
Expand Up @@ -85,6 +85,16 @@
<version>1.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
<version>1.25</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plain-credentials</artifactId>
<version>1.1</version>
</dependency>
<!-- only here to prevent from being included inside hpi for hudson parent, not needed by project at all -->
<dependency>
<groupId>log4j</groupId>
Expand Down
63 changes: 57 additions & 6 deletions src/main/java/jenkins/plugins/slack/SlackNotifier.java
@@ -1,5 +1,7 @@
package jenkins.plugins.slack;

import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.domains.HostnameRequirement;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
Expand All @@ -8,15 +10,18 @@
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.listeners.ItemListener;
import hudson.security.ACL;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import jenkins.model.JenkinsLocationConfiguration;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
Expand All @@ -27,12 +32,15 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials;

public class SlackNotifier extends Notifier {

private static final Logger logger = Logger.getLogger(SlackNotifier.class.getName());

private String teamDomain;
private String authToken;
private String authTokenCredentialId;
private String buildServerUrl;
private String room;
private String sendAs;
Expand Down Expand Up @@ -66,6 +74,10 @@ public String getAuthToken() {
return authToken;
}

public String getAuthTokenCredentialId() {
return authTokenCredentialId;
}

public String getBuildServerUrl() {
if(buildServerUrl == null || buildServerUrl == "") {
JenkinsLocationConfiguration jenkinsConfig = new JenkinsLocationConfiguration();
Expand Down Expand Up @@ -129,14 +141,15 @@ public String getCustomMessage() {
}

@DataBoundConstructor
public SlackNotifier(final String teamDomain, final String authToken, final String room, final String buildServerUrl,
public SlackNotifier(final String teamDomain, final String authToken, final String authTokenCredentialId, final String room, final String buildServerUrl,
final String sendAs, final boolean startNotification, final boolean notifyAborted, final boolean notifyFailure,
final boolean notifyNotBuilt, final boolean notifySuccess, final boolean notifyUnstable, final boolean notifyBackToNormal,
final boolean notifyRepeatedFailure, final boolean includeTestSummary, CommitInfoChoice commitInfoChoice,
boolean includeCustomMessage, String customMessage) {
super();
this.teamDomain = teamDomain;
this.authToken = authToken;
this.authTokenCredentialId = StringUtils.trim(authTokenCredentialId);
this.buildServerUrl = buildServerUrl;
this.room = room;
this.sendAs = sendAs;
Expand Down Expand Up @@ -167,6 +180,10 @@ public SlackService newSlackService(AbstractBuild r, BuildListener listener) {
if (StringUtils.isEmpty(authToken)) {
authToken = getDescriptor().getToken();
}
String authTokenCredentialId = this.authTokenCredentialId;
if (StringUtils.isEmpty(authTokenCredentialId)) {
authTokenCredentialId = getDescriptor().getTokenCredentialId();
}
String room = this.room;
if (StringUtils.isEmpty(room)) {
room = getDescriptor().getRoom();
Expand All @@ -181,9 +198,10 @@ public SlackService newSlackService(AbstractBuild r, BuildListener listener) {
}
teamDomain = env.expand(teamDomain);
authToken = env.expand(authToken);
authTokenCredentialId = env.expand(authTokenCredentialId);
room = env.expand(room);

return new StandardSlackService(teamDomain, authToken, room);
return new StandardSlackService(teamDomain, authToken, authTokenCredentialId, room);
}

@Override
Expand All @@ -210,6 +228,7 @@ public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {

private String teamDomain;
private String token;
private String tokenCredentialId;
private String room;
private String buildServerUrl;
private String sendAs;
Expand All @@ -228,6 +247,10 @@ public String getToken() {
return token;
}

public String getTokenCredentialId() {
return tokenCredentialId;
}

public String getRoom() {
return room;
}
Expand All @@ -246,6 +269,27 @@ public String getSendAs() {
return sendAs;
}

@SuppressWarnings("unused")
public ListBoxModel doFillTokenCredentialIdItems() {
if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
return new ListBoxModel();
}
return new StandardListBoxModel()
.withEmptySelection()
.withAll(lookupCredentials(
StringCredentials.class,
Jenkins.getInstance(),
ACL.SYSTEM,
new HostnameRequirement("*.slack.com"))
);
}

//WARN users that they should not use the plain/exposed token, but rather the token credential id
public FormValidation doCheckToken(@QueryParameter String value) {
//always show the warning - TODO investigate if there is a better way to handle this
return FormValidation.warning("Exposing your Integration Token is a security risk. Please use the Integration Token Credential ID");
}

public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
Expand All @@ -254,6 +298,7 @@ public boolean isApplicable(Class<? extends AbstractProject> aClass) {
public SlackNotifier newInstance(StaplerRequest sr, JSONObject json) {
String teamDomain = sr.getParameter("slackTeamDomain");
String token = sr.getParameter("slackToken");
String tokenCredentialId = json.getString("tokenCredentialId");
String room = sr.getParameter("slackRoom");
boolean startNotification = "true".equals(sr.getParameter("slackStartNotification"));
boolean notifySuccess = "true".equals(sr.getParameter("slackNotifySuccess"));
Expand All @@ -267,7 +312,7 @@ public SlackNotifier newInstance(StaplerRequest sr, JSONObject json) {
CommitInfoChoice commitInfoChoice = CommitInfoChoice.forDisplayName(sr.getParameter("slackCommitInfoChoice"));
boolean includeCustomMessage = "on".equals(sr.getParameter("includeCustomMessage"));
String customMessage = sr.getParameter("customMessage");
return new SlackNotifier(teamDomain, token, room, buildServerUrl, sendAs, startNotification, notifyAborted,
return new SlackNotifier(teamDomain, token, tokenCredentialId, room, buildServerUrl, sendAs, startNotification, notifyAborted,
notifyFailure, notifyNotBuilt, notifySuccess, notifyUnstable, notifyBackToNormal, notifyRepeatedFailure,
includeTestSummary, commitInfoChoice, includeCustomMessage, customMessage);
}
Expand All @@ -276,6 +321,7 @@ public SlackNotifier newInstance(StaplerRequest sr, JSONObject json) {
public boolean configure(StaplerRequest sr, JSONObject formData) throws FormException {
teamDomain = sr.getParameter("slackTeamDomain");
token = sr.getParameter("slackToken");
tokenCredentialId = formData.getJSONObject("slack").getString("tokenCredentialId");
room = sr.getParameter("slackRoom");
buildServerUrl = sr.getParameter("slackBuildServerUrl");
sendAs = sr.getParameter("slackSendAs");
Expand All @@ -290,8 +336,8 @@ public boolean configure(StaplerRequest sr, JSONObject formData) throws FormExce
return super.configure(sr, formData);
}

SlackService getSlackService(final String teamDomain, final String authToken, final String room) {
return new StandardSlackService(teamDomain, authToken, room);
SlackService getSlackService(final String teamDomain, final String authToken, final String authTokenCredentialId, final String room) {
return new StandardSlackService(teamDomain, authToken, authTokenCredentialId, room);
}

@Override
Expand All @@ -301,6 +347,7 @@ public String getDisplayName() {

public FormValidation doTestConnection(@QueryParameter("slackTeamDomain") final String teamDomain,
@QueryParameter("slackToken") final String authToken,
@QueryParameter("tokenCredentialId") final String authTokenCredentialId,
@QueryParameter("slackRoom") final String room,
@QueryParameter("slackBuildServerUrl") final String buildServerUrl) throws FormException {
try {
Expand All @@ -312,6 +359,10 @@ public FormValidation doTestConnection(@QueryParameter("slackTeamDomain") final
if (StringUtils.isEmpty(targetToken)) {
targetToken = this.token;
}
String targetTokenCredentialId = authTokenCredentialId;
if (StringUtils.isEmpty(targetTokenCredentialId)) {
targetTokenCredentialId = this.tokenCredentialId;
}
String targetRoom = room;
if (StringUtils.isEmpty(targetRoom)) {
targetRoom = this.room;
Expand All @@ -320,7 +371,7 @@ public FormValidation doTestConnection(@QueryParameter("slackTeamDomain") final
if (StringUtils.isEmpty(targetBuildServerUrl)) {
targetBuildServerUrl = this.buildServerUrl;
}
SlackService testSlackService = getSlackService(targetDomain, targetToken, targetRoom);
SlackService testSlackService = getSlackService(targetDomain, targetToken, targetTokenCredentialId, targetRoom);
String message = "Slack/Jenkins plugin: you're all set on " + targetBuildServerUrl;
boolean success = testSlackService.publish(message, "good");
return success ? FormValidation.ok("Success") : FormValidation.error("Failure");
Expand Down
39 changes: 37 additions & 2 deletions src/main/java/jenkins/plugins/slack/StandardSlackService.java
@@ -1,19 +1,32 @@
package jenkins.plugins.slack;

import hudson.security.ACL;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;

import org.jenkinsci.plugins.plaincredentials.StringCredentials;

import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;

import org.json.JSONObject;
import org.json.JSONArray;

import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import jenkins.model.Jenkins;
import hudson.ProxyConfiguration;

import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.lang.StringUtils;

public class StandardSlackService implements SlackService {

Expand All @@ -22,12 +35,14 @@ public class StandardSlackService implements SlackService {
private String host = "slack.com";
private String teamDomain;
private String token;
private String authTokenCredentialId;
private String[] roomIds;

public StandardSlackService(String teamDomain, String token, String roomId) {
public StandardSlackService(String teamDomain, String token, String authTokenCredentialId, String roomId) {
super();
this.teamDomain = teamDomain;
this.token = token;
this.authTokenCredentialId = StringUtils.trim(authTokenCredentialId);
this.roomIds = roomId.split("[,; ]+");
}

Expand All @@ -38,7 +53,7 @@ public boolean publish(String message) {
public boolean publish(String message, String color) {
boolean result = true;
for (String roomId : roomIds) {
String url = "https://" + teamDomain + "." + host + "/services/hooks/jenkins-ci?token=" + token;
String url = "https://" + teamDomain + "." + host + "/services/hooks/jenkins-ci?token=" + getTokenToUse();
logger.info("Posting: to " + roomId + " on " + teamDomain + " using " + url +": " + message + " " + color);
HttpClient client = getHttpClient();
PostMethod post = new PostMethod(url);
Expand Down Expand Up @@ -89,6 +104,26 @@ public boolean publish(String message, String color) {
return result;
}

private String getTokenToUse() {
if (authTokenCredentialId != null && !authTokenCredentialId.isEmpty()) {
StringCredentials credentials = lookupCredentials(authTokenCredentialId);
if (credentials != null) {
logger.info("Using Integration Token Credential ID.");
return credentials.getSecret().getPlainText();
}
}

logger.info("Using Integration Token.");

return token;
}

private StringCredentials lookupCredentials(String credentialId) {
List<StringCredentials> credentials = CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.getInstance(), ACL.SYSTEM, Collections.<DomainRequirement>emptyList());
CredentialsMatcher matcher = CredentialsMatchers.withId(credentialId);
return CredentialsMatchers.firstOrNull(credentials, matcher);
}

protected HttpClient getHttpClient() {
HttpClient client = new HttpClient();
if (Jenkins.getInstance() != null) {
Expand Down

0 comments on commit 40a0ac4

Please sign in to comment.