Skip to content

Commit

Permalink
slack-support
Browse files Browse the repository at this point in the history
  • Loading branch information
adriantr committed Mar 31, 2023
1 parent 3919653 commit cb8dcdb
Show file tree
Hide file tree
Showing 8 changed files with 530 additions and 177 deletions.
233 changes: 233 additions & 0 deletions sources/jitaccess.iml

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion sources/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
specific language governing permissions and limitations
under the License.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.solutions</groupId>
Expand Down Expand Up @@ -84,6 +85,16 @@
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>slack-api-client</artifactId>
<version>1.27.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version>
</dependency>


<!-- Test dependencies -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// Copyright 2023 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//

package com.google.solutions.jitaccess.core.adapters;

import java.net.HttpURLConnection;

import com.google.common.base.Preconditions;
import com.slack.api.Slack;
import com.slack.api.webhook.Payload;

public class SlackAdapter {
private final Slack slack;
private final Options options;

public SlackAdapter(Options options) {
Preconditions.checkNotNull(options, "options");
this.options = options;
this.slack = Slack.getInstance();
}

public void sendSlackMessage(String content) throws SlackException {
Preconditions.checkNotNull(content, "content");

try {
var response = slack.send(this.options.slackHookUrl, Payload.builder().text(content).build());
if (!response.getCode().equals(HttpURLConnection.HTTP_OK)) {
throw new SlackException(
"The Slack notification could not be sent. HTTP Code: " + response.getCode(), null);
}
} catch (Exception e) {
throw new SlackException("The Slack notification could not be sent", e);
}
}

public static class Options {
private final String slackHookUrl;

public Options(String slackHookUrl) {
Preconditions.checkNotNull(slackHookUrl, "slackHookUrl");

this.slackHookUrl = slackHookUrl;
}
}

public static class SlackException extends Exception {
public SlackException(String message, Throwable cause) {
super(message, cause);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
// Copyright 2022 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
Expand All @@ -25,10 +25,10 @@
import com.google.common.escape.Escaper;
import com.google.common.html.HtmlEscapers;
import com.google.solutions.jitaccess.core.AccessException;
import com.google.solutions.jitaccess.core.adapters.SlackAdapter;
import com.google.solutions.jitaccess.core.adapters.SmtpAdapter;
import com.google.solutions.jitaccess.core.data.UserId;

import javax.enterprise.context.ApplicationScoped;
import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
Expand All @@ -41,6 +41,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.enterprise.context.ApplicationScoped;

/**
* Service for notifying users about activation requests..
Expand All @@ -53,33 +54,31 @@ public abstract class NotificationService {

/**
* Load a resource from a JAR resource.
*
* @return null if not found.
*/
public static String loadResource(String resourceName) throws NotificationException{
try (var stream = NotificationService.class
.getClassLoader()
.getResourceAsStream(resourceName)) {
public static String loadResource(String resourceName) throws NotificationException {
try (var stream =
NotificationService.class.getClassLoader().getResourceAsStream(resourceName)) {

if (stream == null) {
return null;
}

var content = stream.readAllBytes();
if (content.length > 3 &&
content[0] == (byte)0xEF &&
content[1] == (byte)0xBB &&
content[2] == (byte)0xBF) {
if (content.length > 3
&& content[0] == (byte) 0xEF
&& content[1] == (byte) 0xBB
&& content[2] == (byte) 0xBF) {

//
// Strip UTF-8 BOM.
//
return new String(content, 3, content.length - 3);
}
else {
} else {
return new String(content);
}
}
catch (IOException e) {
} catch (IOException e) {
throw new NotificationException(
String.format("Reading the template %s from the JAR file failed", resourceName), e);
}
Expand All @@ -96,10 +95,8 @@ public static class MailNotificationService extends NotificationService {
private final Options options;
private final SmtpAdapter smtpAdapter;

public MailNotificationService(
SmtpAdapter smtpAdapter,
Options options
) {
public MailNotificationService(SmtpAdapter smtpAdapter,
Options options) {
Preconditions.checkNotNull(smtpAdapter);
Preconditions.checkNotNull(options);

Expand All @@ -116,20 +113,18 @@ public boolean canSendNotifications() {
public void sendNotification(Notification notification) throws NotificationException {
Preconditions.checkNotNull(notification, "notification");

var htmlTemplate = loadResource(
String.format("notifications/%s.html", notification.getTemplateId()));
var htmlTemplate =
loadResource(String.format("notifications/%s.html", notification.getTemplateId()));
if (htmlTemplate == null) {
//
// Unknown kind of notification, ignore.
//
return;
}

var formattedMessage = new NotificationTemplate(
htmlTemplate,
this.options.timeZone,
HtmlEscapers.htmlEscaper())
.format(notification);
var formattedMessage =
new NotificationTemplate(htmlTemplate, this.options.timeZone, HtmlEscapers.htmlEscaper())
.format(notification);

try {
this.smtpAdapter.sendMail(
Expand All @@ -140,13 +135,45 @@ public void sendNotification(Notification notification) throws NotificationExcep
notification.isReply()
? EnumSet.of(SmtpAdapter.Flags.REPLY)
: EnumSet.of(SmtpAdapter.Flags.NONE));
}
catch (SmtpAdapter.MailException | AccessException | IOException e) {
} catch (SmtpAdapter.MailException | AccessException | IOException e) {
throw new NotificationException("The notification could not be sent", e);
}
}
}

public static class SlackNotificationService extends NotificationService {
private final SlackAdapter slackAdapter;

public SlackNotificationService(SlackAdapter slackAdapter) {
Preconditions.checkNotNull(slackAdapter);

this.slackAdapter = slackAdapter;
}

public boolean canSendNotifications() {
return true;
}

@Override
public void sendNotification(Notification notification) throws NotificationException {
Preconditions.checkNotNull(notification, "notification");

var message =
String.format(
"User `%s` has activated role `%s` in project `%s`.\n\nJustification: _%s_",
notification.properties.get("BENEFICIARY"),
notification.properties.get("ROLE"),
notification.properties.get("PROJECT_ID"),
notification.properties.get("JUSTIFICATION"));

try {
this.slackAdapter.sendSlackMessage(message);
} catch (SlackAdapter.SlackException e) {
throw new NotificationException("The message cound not be sent", e);
}
}
}

/**
* Concrete class that prints notifications to STDOUT. Useful for local development only.
*/
Expand All @@ -166,10 +193,10 @@ public void sendNotification(Notification notification) throws NotificationExcep
}

/**
* Generic notification. The object contains the data for a notification,
* but doesn't define its format.
* Generic notification. The object contains the data for a notification, but doesn't define its
* format.
*/
public static abstract class Notification {
public abstract static class Notification {
private final Collection<UserId> toRecipients;
private final Collection<UserId> ccRecipients;
private final String subject;
Expand All @@ -185,8 +212,7 @@ protected boolean isReply() {
protected Notification(
Collection<UserId> toRecipients,
Collection<UserId> ccRecipients,
String subject
) {
String subject) {
Preconditions.checkNotNull(toRecipients, "toRecipients");
Preconditions.checkNotNull(ccRecipients, "ccRecipients");
Preconditions.checkNotNull(subject, "subject");
Expand All @@ -202,9 +228,7 @@ public String toString() {
"Notification to %s: %s\n\n%s",
this.toRecipients.stream().map(e -> e.email).collect(Collectors.joining(", ")),
this.subject,
this.properties
.entrySet()
.stream()
this.properties.entrySet().stream()
.map(e -> String.format(" %s: %s", e.getKey(), e.getValue()))
.collect(Collectors.joining("\n", "", "")));
}
Expand All @@ -218,11 +242,9 @@ public static class NotificationTemplate {
private final Escaper escaper;
private final ZoneId timezoneId;

public NotificationTemplate(
String template,
ZoneId timezoneId,
Escaper escaper
) {
public NotificationTemplate(String template,
ZoneId timezoneId,
Escaper escaper) {
Preconditions.checkNotNull(template, "template");
Preconditions.checkNotNull(timezoneId, "timezoneId");
Preconditions.checkNotNull(escaper, "escaper");
Expand All @@ -246,12 +268,11 @@ public String format(NotificationService.Notification notification) {
//
// Apply time zone and convert to string.
//
propertyValue = OffsetDateTime
.ofInstant((Instant)property.getValue(), this.timezoneId)
.truncatedTo(ChronoUnit.SECONDS)
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
else {
propertyValue =
OffsetDateTime.ofInstant((Instant) property.getValue(), this.timezoneId)
.truncatedTo(ChronoUnit.SECONDS)
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
} else {
//
// Convert to a safe string.
//
Expand All @@ -277,7 +298,8 @@ public Options(ZoneId timeZone) {
}

public static class NotificationException extends Exception {
public NotificationException(String message, Throwable cause) {
public NotificationException(String message,
Throwable cause) {
super(message, cause);
}
}
Expand Down
Loading

0 comments on commit cb8dcdb

Please sign in to comment.