Skip to content

Commit

Permalink
feat: authorization for webhooks (#177)
Browse files Browse the repository at this point in the history
Refs: #169
  • Loading branch information
grigoriev authored Sep 2, 2024
1 parent c2f3b69 commit 4da2ee2
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 100 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ ch.sbb.polarion.extension.pdf-exporter.internalizeExternalCss=true
## Extension Configuration

1. On the top of the project's navigation pane click ⚙ (Actions) ➙ 🔧 Administration. Project's administration page will be opened.
2. On the administration's navigation pane select `PDF Export`. There are 5 sub-menus with different configuration options for PDF Exporter.
3. For 5 of these options (Cover page, Header and Footer, Localization, Webhooks and Filename template) `Quick Help` section available with option short description. For the rest 2
2. On the administration's navigation pane select `PDF Export`. There are expandable sub-menus with different configuration options for PDF Exporter.
3. For some of these options (Cover page, Header and Footer, Localization, Webhooks and Filename template) `Quick Help` section available with option short description. For the rest
(Style package, Stylesheets) there's no `Quick Help` section as their content is self-evident.
4. To change configuration of PDF exporter extension just edit corresponding section and press `Save` button.
4. To change configuration of PDF Exporter extension just edit corresponding section and press `Save` button.

## Usage

Expand Down
2 changes: 1 addition & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@
"description": "default response"
}
},
"summary": "Returns boolean value telling if webhooks are enabled or not",
"summary": "Gets webhooks status - if they are enabled or not",
"tags": [
"Utility resources"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
import ch.sbb.polarion.extension.pdf.exporter.rest.model.conversion.DocumentType;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.conversion.ExportParams;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.headerfooter.HeaderFooterModel;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.hooks.WebhooksModel;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.webhooks.AuthType;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.webhooks.WebhookConfig;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.webhooks.WebhooksModel;
import ch.sbb.polarion.extension.pdf.exporter.service.PdfExporterPolarionService;
import ch.sbb.polarion.extension.pdf.exporter.settings.CssSettings;
import ch.sbb.polarion.extension.pdf.exporter.settings.HeaderFooterSettings;
import ch.sbb.polarion.extension.pdf.exporter.settings.WebhooksSettings;
import ch.sbb.polarion.extension.pdf.exporter.settings.LocalizationSettings;
import ch.sbb.polarion.extension.pdf.exporter.settings.WebhooksSettings;
import ch.sbb.polarion.extension.pdf.exporter.util.EnumValuesProvider;
import ch.sbb.polarion.extension.pdf.exporter.util.HtmlLogger;
import ch.sbb.polarion.extension.pdf.exporter.util.HtmlProcessor;
Expand All @@ -27,11 +29,12 @@
import ch.sbb.polarion.extension.pdf.exporter.util.placeholder.PlaceholderProcessor;
import ch.sbb.polarion.extension.pdf.exporter.util.velocity.VelocityEvaluator;
import ch.sbb.polarion.extension.pdf.exporter.weasyprint.WeasyPrintOptions;
import com.fasterxml.jackson.databind.ObjectMapper;
import ch.sbb.polarion.extension.pdf.exporter.weasyprint.service.WeasyPrintServiceConnector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.polarion.alm.tracker.model.ITrackerProject;
import com.polarion.core.util.StringUtils;
import com.polarion.core.util.logging.Logger;
import com.polarion.platform.internal.security.UserAccountVault;
import lombok.AllArgsConstructor;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
Expand All @@ -43,12 +46,15 @@
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -154,33 +160,37 @@ public byte[] convertToPdf(@NotNull ExportParams exportParams, @Nullable ExportM

WebhooksModel webhooksModel = new WebhooksSettings().load(exportParams.getProjectId(), SettingId.fromName(exportParams.getWebhooks()));
String result = htmlContent;
for (String webhook : webhooksModel.getWebhooks()) {
result = applyWebhook(webhook, exportParams, result);
for (WebhookConfig webhookConfig : webhooksModel.getWebhookConfigs()) {
result = applyWebhook(webhookConfig, exportParams, result);
}
return result;
}

private @NotNull String applyWebhook(@NotNull String webhook, @NotNull ExportParams exportParams, @NotNull String htmlContent) {
private @NotNull String applyWebhook(@NotNull WebhookConfig webhookConfig, @NotNull ExportParams exportParams, @NotNull String htmlContent) {
Client client = null;
try {
client = ClientBuilder.newClient();
WebTarget webTarget = client.target(webhook).register(MultiPartFeature.class);
WebTarget webTarget = client.target(webhookConfig.getUrl()).register(MultiPartFeature.class);

FormDataMultiPart multipart = new FormDataMultiPart();
multipart.bodyPart(new FormDataBodyPart("exportParams", new ObjectMapper().writeValueAsString(exportParams), MediaType.APPLICATION_JSON_TYPE));
multipart.bodyPart(new FormDataBodyPart("html", htmlContent.getBytes(StandardCharsets.UTF_8), MediaType.APPLICATION_OCTET_STREAM_TYPE));

try (Response response = webTarget.request(MediaType.TEXT_PLAIN).post(Entity.entity(multipart, multipart.getMediaType()))) {
Invocation.Builder requestBuilder = webTarget.request(MediaType.TEXT_PLAIN);

addAuthHeader(webhookConfig, requestBuilder);

try (Response response = requestBuilder.post(Entity.entity(multipart, multipart.getMediaType()))) {
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
try (InputStream inputStream = response.readEntity(InputStream.class)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
} else {
logger.error(String.format("Could not get proper response from webhook [%s]: response status %s", webhook, response.getStatus()));
logger.error(String.format("Could not get proper response from webhook [%s]: response status %s", webhookConfig.getUrl(), response.getStatus()));
}
}
} catch (Exception e) {
logger.error(String.format("Could not get response from webhook [%s]", webhook), e);
logger.error(String.format("Could not get response from webhook [%s]", webhookConfig.getUrl()), e);
} finally {
if (client != null) {
client.close();
Expand All @@ -191,6 +201,31 @@ public byte[] convertToPdf(@NotNull ExportParams exportParams, @Nullable ExportM
return htmlContent;
}

private static void addAuthHeader(@NotNull WebhookConfig webhookConfig, @NotNull Invocation.Builder requestBuilder) {
if (webhookConfig.getAuthType() == null || webhookConfig.getAuthTokenName() == null) {
return;
}

String authInfoFromUserAccountVault = getAuthInfoFromUserAccountVault(webhookConfig.getAuthType(), webhookConfig.getAuthTokenName());
if (authInfoFromUserAccountVault == null) {
return;
}

requestBuilder.header(HttpHeaders.AUTHORIZATION, webhookConfig.getAuthType().getAuthHeaderPrefix() + " " + authInfoFromUserAccountVault);
}

private static @Nullable String getAuthInfoFromUserAccountVault(@NotNull AuthType authType, @NotNull String authTokenName) {
@NotNull UserAccountVault.Credentials credentials = UserAccountVault.getInstance().getCredentialsForKey(authTokenName);

return switch (authType) {
case BASIC_AUTH -> {
String authInfo = credentials.getUser() + ":" + credentials.getPassword();
yield Base64.getEncoder().encodeToString(authInfo.getBytes());
}
case BEARER_TOKEN -> credentials.getPassword();
};
}

@VisibleForTesting
byte[] generatePdf(
LiveDocHelper.DocumentData documentData,
Expand Down Expand Up @@ -219,8 +254,8 @@ String postProcessDocumentContent(@NotNull ExportParams exportParams, @Nullable
@NotNull
@VisibleForTesting
String composeHtml(@NotNull String documentName,
@NotNull HtmlData htmlData,
@NotNull ExportParams exportParams) {
@NotNull HtmlData htmlData,
@NotNull ExportParams exportParams) {
String content = htmlData.headerFooterContent
+ "<div class='content'>" + htmlData.documentContent + "</div>";
return pdfTemplateProcessor.processUsing(exportParams, documentName, htmlData.cssContent, content);
Expand All @@ -236,9 +271,9 @@ String getCssContent(
String listStyles = new PdfExporterListStyleProvider(exportParams.getNumberedListStyles()).getStyle();
String css = pdfStyles
+ (exportParams.getHeadersColor() != null ?
" h1, h2, h3, h4, h5, h6, .content .title {"
+ " color: " + exportParams.getHeadersColor() + ";"
+ " }"
" h1, h2, h3, h4, h5, h6, .content .title {" +
" color: " + exportParams.getHeadersColor() + ";" +
" }"
: "")
+ listStyles;

Expand Down Expand Up @@ -279,5 +314,6 @@ private String appendWikiCss(String css) {
return css + System.lineSeparator() + ScopeUtils.getFileContent("default/wiki.css");
}

record HtmlData(String cssContent, String documentContent, String headerFooterContent) {}
record HtmlData(String cssContent, String documentContent, String headerFooterContent) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.webhooks;

import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.Getter;

@Getter
public enum AuthType {
BEARER_TOKEN("Bearer"),
BASIC_AUTH("Basic");

private final String authHeaderPrefix;

AuthType(String authHeaderPrefix) {
this.authHeaderPrefix = authHeaderPrefix;
}

@JsonCreator
public static AuthType forName(String name) {
for (AuthType authType : values()) {
if (authType.name().equalsIgnoreCase(name)) {
return authType;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.webhooks;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebhookConfig {
private String url;
private AuthType authType;
private String authTokenName;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.hooks;
package ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.webhooks;

import ch.sbb.polarion.extension.generic.settings.SettingsModel;
import com.fasterxml.jackson.annotation.JsonInclude;
Expand All @@ -25,22 +25,22 @@
public class WebhooksModel extends SettingsModel {
public static final String WEBHOOKS_ENTRY_NAME = "WEBHOOKS";

private List<String> webhooks = new ArrayList<>();
private List<WebhookConfig> webhookConfigs = new ArrayList<>();

@Override
protected String serializeModelData() {
return serializeEntry(WEBHOOKS_ENTRY_NAME, webhooks);
return serializeEntry(WEBHOOKS_ENTRY_NAME, webhookConfigs);
}

@Override
protected void deserializeModelData(String serializedString) {
String webhooksString = deserializeEntry(WEBHOOKS_ENTRY_NAME, serializedString);
if (webhooksString != null) {
String content = deserializeEntry(WEBHOOKS_ENTRY_NAME, serializedString);
if (content != null) {
try {
String[] webhooksArray = new ObjectMapper().readValue(webhooksString, String[].class);
webhooks = new ArrayList<>(Arrays.asList(webhooksArray));
WebhookConfig[] configs = new ObjectMapper().readValue(content, WebhookConfig[].class);
this.webhookConfigs = new ArrayList<>(Arrays.asList(configs));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Webhooks value couldn't be parsed", e);
throw new IllegalArgumentException(WEBHOOKS_ENTRY_NAME + " value couldn't be parsed", e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import ch.sbb.polarion.extension.generic.settings.GenericNamedSettings;
import ch.sbb.polarion.extension.generic.settings.SettingsService;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.hooks.WebhooksModel;
import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.webhooks.WebhooksModel;
import org.jetbrains.annotations.NotNull;

public class WebhooksSettings extends GenericNamedSettings<WebhooksModel> {
Expand Down
Loading

0 comments on commit 4da2ee2

Please sign in to comment.