diff --git a/src/main/java/org/karnak/backend/data/entity/DestinationEntity.java b/src/main/java/org/karnak/backend/data/entity/DestinationEntity.java index 007143ac..b0a6a238 100644 --- a/src/main/java/org/karnak/backend/data/entity/DestinationEntity.java +++ b/src/main/java/org/karnak/backend/data/entity/DestinationEntity.java @@ -141,9 +141,6 @@ public class DestinationEntity implements Serializable { // mandatory[type=stow] private String url; - // credentials of the STOW-RS service (format is "user:password"). - private String urlCredentials; - // headers for HTTP request. private String headers; @@ -190,7 +187,6 @@ protected DestinationEntity(DestinationType destinationType) { this.port = 0; this.useaetdest = Boolean.FALSE; this.url = ""; - this.urlCredentials = ""; this.headers = ""; this.transcodeOnlyUncompressed = false; @@ -215,11 +211,10 @@ public static DestinationEntity ofStowEmpty() { return new DestinationEntity(DestinationType.stow); } - public static DestinationEntity ofStow(String description, String url, String urlCredentials, String headers) { + public static DestinationEntity ofStow(String description, String url, String headers) { DestinationEntity destinationEntity = new DestinationEntity(DestinationType.stow); destinationEntity.setDescription(description); destinationEntity.setUrl(url); - destinationEntity.setUrlCredentials(urlCredentials); destinationEntity.setHeaders(headers); return destinationEntity; } @@ -379,14 +374,6 @@ public void setUrl(String url) { this.url = url; } - public String getUrlCredentials() { - return urlCredentials; - } - - public void setUrlCredentials(String urlCredentials) { - this.urlCredentials = urlCredentials; - } - @Size(max = 4096, message = "Headers has more than 4096 characters") public String getHeaders() { return headers; @@ -572,7 +559,6 @@ public boolean matchesFilter(String filterText) { || contains(hostname, filterText) // || equals(port, filterText) // || contains(url, filterText) // - || contains(urlCredentials, filterText) // || contains(headers, filterText); } @@ -599,7 +585,7 @@ public String toString() { + ", notify=" + notify + ", notifyObjectErrorPrefix=" + notifyObjectErrorPrefix + ", notifyObjectPattern=" + notifyObjectPattern + ", notifyObjectValues=" + notifyObjectValues + ", notifyInterval=" + notifyInterval + ", url=" + url - + ", urlCredentials=" + urlCredentials + ", headers=" + headers + "]"; + + ", headers=" + headers + "]"; } } return "Destination [id=" + id + ", description=" + description + ", type=" + destinationType + ", notify=" diff --git a/src/main/java/org/karnak/frontend/forwardnode/edit/destination/component/AuthHeadersGenerationDialog.java b/src/main/java/org/karnak/frontend/forwardnode/edit/destination/component/AuthHeadersGenerationDialog.java new file mode 100644 index 00000000..6037b671 --- /dev/null +++ b/src/main/java/org/karnak/frontend/forwardnode/edit/destination/component/AuthHeadersGenerationDialog.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2021 Karnak Team and other contributors. + * + * This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache + * License, Version 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package org.karnak.frontend.forwardnode.edit.destination.component; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.select.Select; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.dom.Style; + +import java.util.Base64; + +public class AuthHeadersGenerationDialog extends Dialog { + + public static final String AUTHORIZATION_TAG = "Authorization"; + + public static final String TITLE = "Generate Authorization Header"; + + private static final String BASIC_AUTH = "Basic Auth"; + + private static final String OAUTH2 = "OAuth 2"; + + private static final String FIELD_WIDTH = "400px"; + + private static final String BUTTON_WIDTH = "200px"; + + private Select authTypeSelect; + + private final String[] authTypeSelectValues = { BASIC_AUTH, OAUTH2 }; + + private Button cancelButton; + + private Button generateButton; + + private Div divContent; + + private Div divTitle; + + private Div divSelectBox; + + private FormLayout basicForm; + + private TextField basicUsername; + + private TextField basicPassword; + + private FormLayout oauthForm; + + private TextField oauthToken; + + private final FormSTOW parentForm; + + public AuthHeadersGenerationDialog(FormSTOW parentForm) { + this.parentForm = parentForm; + + removeAll(); + setWidth("50%"); + + setElement(); + HorizontalLayout horizontalLayout = new HorizontalLayout(cancelButton, generateButton); + horizontalLayout.setWidthFull(); + horizontalLayout.getStyle().set("justify-content", "flex-end").set("margin-top", "20px"); + add(divTitle, divSelectBox, divContent, horizontalLayout); + } + + private void setElement() { + divTitle = new Div(); + divTitle.setText(TITLE); + divTitle.getStyle().set("font-size", "large").set("font-weight", "bolder").set("padding-bottom", "10px"); + + divContent = new Div(); + divSelectBox = new Div(); + authTypeSelect = new Select<>(); + authTypeSelect.setItems(authTypeSelectValues); + authTypeSelect.setLabel("Authorization Type"); + authTypeSelect.setErrorMessage("This field is mandatory"); + authTypeSelect.setWidth(FIELD_WIDTH); + authTypeSelect.setEmptySelectionAllowed(false); + authTypeSelect.addValueChangeListener(value -> { + displayAuthTypeForm(value.getValue()); + }); + + divSelectBox.add(authTypeSelect); + + generateButton = new Button("Generate Headers", event -> { + if (validateFields(authTypeSelect.getValue())) { + generateAuthHeaders(authTypeSelect.getValue()); + } + }); + generateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + generateButton.setWidth(BUTTON_WIDTH); + cancelButton = new Button("Cancel", event -> close()); + cancelButton.setWidth(BUTTON_WIDTH); + + buildForms(); + } + + /** + * Display the form according to the Authentication Type selected in the select box + * @param value : String corresponding to the Authentication Type chosen in the select box + */ + private void displayAuthTypeForm(String value) { + divContent.removeAll(); + switch(value) { + case BASIC_AUTH: + divContent.add(basicForm); + break; + case OAUTH2: + divContent.add(oauthForm); + break; + } + } + + /** + * Validation method for the authorization type select box, and the fields in the corresponding form + * @param authType : String corresponding to the Authorization Type chosen in the select box + * @return true if the form is valid, false otherwise + */ + private boolean validateFields(String authType) { + // Mark the authorization type select box as invalid if a value is not selected + if (authTypeSelect.isEmpty()) { + authTypeSelect.setInvalid(true); + return false; // set the validation as failed + } + // Once the authorization type is chosen, ensure that the proper fields are filled for the generation + switch (authType) { + case BASIC_AUTH: + basicUsername.setInvalid(basicUsername.isEmpty()); + basicPassword.setInvalid(basicPassword.isEmpty()); + return !(basicUsername.isEmpty() || basicPassword.isEmpty()); + case OAUTH2: + oauthToken.setInvalid(oauthToken.isEmpty()); + return !(oauthToken.isEmpty()); + } + return false; + } + + private void buildForms() { + buildBasicAuthForm(); + buildOAuth2Form(); + } + + /** + * Create the elements necessary to render the Basic Auth form using the basicForm instance + */ + private void buildBasicAuthForm() { + basicForm = new FormLayout(); + basicForm.setWidthFull(); + + basicUsername = new TextField(); + basicUsername.setWidth(FIELD_WIDTH); + basicUsername.setRequiredIndicatorVisible(true); + basicUsername.setErrorMessage("This field is required"); + basicUsername.setRequired(true); + basicUsername.setLabel("Username"); + + basicPassword = new TextField(); + basicPassword.setWidth(FIELD_WIDTH); + basicPassword.setRequiredIndicatorVisible(true); + basicPassword.setErrorMessage("This field is required"); + basicPassword.setRequired(true); + basicPassword.setLabel("Password"); + + basicForm.add(basicUsername); + basicForm.add(basicPassword); + } + + /** + * Create the elements necessary to render the OAuth 2 form using the oauthForm instance + */ + private void buildOAuth2Form() { + oauthForm = new FormLayout(); + oauthForm.setWidthFull(); + + oauthToken = new TextField(); + oauthToken.setWidth(FIELD_WIDTH); + oauthToken.setRequiredIndicatorVisible(true); + oauthToken.setErrorMessage("This field is required"); + oauthToken.setLabel("OAuth 2 Token"); + + oauthForm.add(oauthToken); + } + + /** + * Generate the Authorization Header based on the Authorization type and the data entered. + * The headers are appended to the parent form headers field. + * @param authType : String corresponding to the Authorization Type chosen in the select box + */ + private void generateAuthHeaders(String authType) { + StringBuilder sb = new StringBuilder(); + sb.append(AUTHORIZATION_TAG); + sb.append("\n"); + sb.append(""); + switch(authType) { + case BASIC_AUTH: + String credentials = basicUsername.getValue() + ":" + basicPassword.getValue(); + sb.append("Basic "); + sb.append(Base64.getEncoder().encodeToString(credentials.getBytes())); + break; + case OAUTH2: + sb.append("Bearer "); + sb.append(oauthToken.getValue()); + break; + } + sb.append(""); + + this.parentForm.appendToHeaders(sb.toString()); + close(); + } +} diff --git a/src/main/java/org/karnak/frontend/forwardnode/edit/destination/component/FormSTOW.java b/src/main/java/org/karnak/frontend/forwardnode/edit/destination/component/FormSTOW.java index ba6234c0..fc619627 100644 --- a/src/main/java/org/karnak/frontend/forwardnode/edit/destination/component/FormSTOW.java +++ b/src/main/java/org/karnak/frontend/forwardnode/edit/destination/component/FormSTOW.java @@ -9,15 +9,18 @@ */ package org.karnak.frontend.forwardnode.edit.destination.component; +import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.dom.Style; import org.apache.commons.lang3.StringUtils; import org.karnak.backend.data.entity.DestinationEntity; import org.karnak.frontend.component.BoxShadowComponent; +import org.karnak.frontend.extid.WarningDialog; import org.karnak.frontend.forwardnode.edit.component.ButtonSaveDeleteCancel; import org.karnak.frontend.kheops.SwitchingAlbumsView; import org.karnak.frontend.util.UIS; @@ -34,7 +37,7 @@ public class FormSTOW extends VerticalLayout { private TextField url; - private TextField urlCredentials; + private Button generateAuthorizationHeaderButton; private TextArea headers; @@ -69,20 +72,20 @@ public void init(Binder binder, ButtonSaveDeleteCancel button this.tagMorphingComponent.init(this.binder); this.filterBySOPClassesForm.init(this.binder); this.destinationCondition.init(binder); - notificationComponent.init(binder); - transferSyntaxComponent.init(this.binder); - transcodeOnlyUncompressedComponent.init(this.binder); + this.notificationComponent.init(binder); + this.transferSyntaxComponent.init(this.binder); + this.transcodeOnlyUncompressedComponent.init(this.binder); this.description = new TextField("Description"); this.url = new TextField("URL"); - this.urlCredentials = new TextField("URL credentials"); + this.generateAuthorizationHeaderButton = new Button(AuthHeadersGenerationDialog.TITLE); this.headers = new TextArea("Headers"); this.switchingAlbumsView = new SwitchingAlbumsView(); this.activate = new Checkbox("Enable destination"); // Define layout VerticalLayout destinationLayout = new VerticalLayout(UIS.setWidthFull(new HorizontalLayout(description)), - destinationCondition, UIS.setWidthFull(new HorizontalLayout(url, urlCredentials)), + destinationCondition, UIS.setWidthFull(new HorizontalLayout(url, generateAuthorizationHeaderButton)), UIS.setWidthFull(headers)); VerticalLayout transferLayout = new VerticalLayout( new HorizontalLayout(transferSyntaxComponent, transcodeOnlyUncompressedComponent)); @@ -108,19 +111,33 @@ public void init(Binder binder, ButtonSaveDeleteCancel button setElements(); setBinder(); + configureGenerateHeadersButton(); + } + + private void configureGenerateHeadersButton() { + this.generateAuthorizationHeaderButton.addClickListener(e -> { + if (this.headers.getValue().contains(AuthHeadersGenerationDialog.AUTHORIZATION_TAG)) { + WarningDialog wd = new WarningDialog("Cannot generate Authorization Header", "The Headers already contain an Authorization tag. Please remove it if you want to generate it.", "Ok"); + wd.open(); + } else { + AuthHeadersGenerationDialog dialog = new AuthHeadersGenerationDialog(this); + dialog.open(); + } + }); } private void setElements() { description.setWidth("100%"); - url.setWidth("50%"); + url.setWidth("70%"); UIS.setTooltip(url, "The destination STOW-RS URL"); - urlCredentials.setWidth("50%"); - UIS.setTooltip(urlCredentials, "Credentials of the STOW-RS service (format is \"user:password\")"); + generateAuthorizationHeaderButton.setWidth("30%"); + generateAuthorizationHeaderButton.getStyle().setAlignSelf(Style.AlignSelf.FLEX_END); headers.setMinHeight("10em"); headers.setWidth("100%"); + headers.getStyle().set("padding", "0px"); UIS.setTooltip(headers, "Headers for HTTP request. Example of format:\nAuthorization\nBearer 1v1pwxT4Ww4DCFzyaMt0NP"); } @@ -136,6 +153,14 @@ private void setBinder() { binder.bindInstanceFields(this); } + public void appendToHeaders(String value) { + String existingHeaders = this.headers.getValue(); + if (!existingHeaders.isEmpty()) { + existingHeaders += "\n"; + } + this.headers.setValue(existingHeaders + value); + } + public DeIdentificationComponent getDeIdentificationComponent() { return deIdentificationComponent; } diff --git a/src/main/resources/db/changelog/changes/db.changelog-1.4.xml b/src/main/resources/db/changelog/changes/db.changelog-1.4.xml index 09ce30d7..412ef004 100644 --- a/src/main/resources/db/changelog/changes/db.changelog-1.4.xml +++ b/src/main/resources/db/changelog/changes/db.changelog-1.4.xml @@ -18,4 +18,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/karnak/backend/data/repo/ForwardNodeRepoTest.java b/src/test/java/org/karnak/backend/data/repo/ForwardNodeRepoTest.java index 6a320e28..0da36901 100644 --- a/src/test/java/org/karnak/backend/data/repo/ForwardNodeRepoTest.java +++ b/src/test/java/org/karnak/backend/data/repo/ForwardNodeRepoTest.java @@ -145,7 +145,7 @@ void testInvalidDestinationStow_URL_mandatory() { ForwardNodeEntity forwardNodeEntity = ForwardNodeEntity.ofEmpty(); forwardNodeEntity.setFwdDescription("description"); forwardNodeEntity.setFwdAeTitle("fwdAeTitle"); - DestinationEntity destinationEntity = DestinationEntity.ofStow("description", null, "urlCredentials", + DestinationEntity destinationEntity = DestinationEntity.ofStow("description", null, "headers"); forwardNodeEntity.addDestination(destinationEntity); @@ -237,7 +237,7 @@ void testWithDestinationStow() { ForwardNodeEntity forwardNodeEntity = ForwardNodeEntity.ofEmpty(); forwardNodeEntity.setFwdDescription("description"); forwardNodeEntity.setFwdAeTitle("fwdAeTitle"); - DestinationEntity destinationEntity = DestinationEntity.ofStow("description", "url", "urlCredentials", + DestinationEntity destinationEntity = DestinationEntity.ofStow("description", "url", "headers"); forwardNodeEntity.addDestination(destinationEntity); entityManager.persistAndFlush(forwardNodeEntity);