diff --git a/MY-PRODUCT-NAME-product/README.md b/MY-PRODUCT-NAME-product/README.md deleted file mode 100644 index ee9062a..0000000 --- a/MY-PRODUCT-NAME-product/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# MY-PRODUCT-NAME - -YOUR DESCRIPTION GOES HERE - -## Demo - -YOUR DEMO DESCRIPTION GOES HERE - -## Setup - -YOUR SETUP DESCRIPTION GOES HERE - -``` -@variables.yaml@ -``` \ No newline at end of file diff --git a/README.md b/README.md index 0f94020..fd8f20a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# MY-PRODUCT-NAME +# Adobe Sign Connector -[![CI Build](https://github.com/axonivy-market/REPO-NAME/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-market/REPO-NAME/actions/workflows/ci.yml) +[![CI Build](https://github.com/axonivy-market/adobesign-connector/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-market/adobesign-connector/actions/workflows/ci.yml) -"YOUR SHORT DESCRIPTION GOES HERE" +The Adobe Sign Connector simplifies the authentication process and enables easy integration and use of Adobe Sign services for electronical signing of documents. -Read our [documentation](MY-PRODUCT-NAME-product/README.md). +Read our [documentation](adobe-esign-connector-product/README.md). diff --git a/adobe-esign-connector-demo/cms/cms_en.yaml b/adobe-esign-connector-demo/cms/cms_en.yaml new file mode 100644 index 0000000..b0129bf --- /dev/null +++ b/adobe-esign-connector-demo/cms/cms_en.yaml @@ -0,0 +1,26 @@ +Dialogs: + com: + axonivy: + connector: + adobe: + esign: + connector: + demo: + Demo: + AdobeSignDemosA: Adobe Sign Demos. Upload PDF file, enter one or multiple signers, create an agreement and sign the document as selected signer + Agreement: Agreement + AgreementID: Agreement ID + AgreementID1: Agreement ID + CreateAgreement: Create Agreement for 1 signer + CreateAgreement1: Create Agreement for 2 signers + Documents: Documents + LoadExisting: Load existing Agreement + LoadExistingToo: Enter Agreement ID and load existing Agreement in case it's already created on Adobe side + PDFFile: PDF file + PDFFileToSign: PDF File to sign + SignAsSigner1: Sign as Signer 1 + SignAsSigner2: Sign as Signer 2 + Signer1Email: Signer 1 email + Signer2Email: Signer 2 email + Signers: Signers + Signing: Signing diff --git a/adobe-esign-connector-demo/config/variables.yaml b/adobe-esign-connector-demo/config/variables.yaml index f6f0321..5dcd07f 100644 --- a/adobe-esign-connector-demo/config/variables.yaml +++ b/adobe-esign-connector-demo/config/variables.yaml @@ -6,8 +6,14 @@ # '/Config/_/variables.yaml # Variables: -# myVariable: value adobe-sign-connector: - host: api.eu2.echosign.com - integrationKey: - returnPage: http://localhost:8081/designer/page/adobe-esign-connector$1/signatureReturn.jsp \ No newline at end of file + host: api.eu2.adobesign.com + returnPage: /designer/page/adobe-esign-connector$1/signatureReturn.jsp + baseUri: https://api.eu2.adobesign.com/oauth/v2 + authenticationUri: https://secure.eu2.adobesign.com/public/oauth/v2 + clientId: '' + clientSecret: '' + permissions: user_read:account user_write:account user_login:account agreement_read:account agreement_write:account agreement_send:account widget_read:account widget_write:account library_read:account library_write:account workflow_read:account workflow_write:account + oauthToken: default-value + accessToken: default-value + integrationKey: '' diff --git a/adobe-esign-connector-demo/pom.xml b/adobe-esign-connector-demo/pom.xml index 9ae08c5..6d4ac07 100644 --- a/adobe-esign-connector-demo/pom.xml +++ b/adobe-esign-connector-demo/pom.xml @@ -4,8 +4,11 @@ 4.0.0 com.axonivy.connector.adobe.esign adobe-esign-connector-demo - 1.0.0-SNAPSHOT + 10.0.11-SNAPSHOT iar + + 10.0.6 + com.axonivy.connector.adobe.esign @@ -19,7 +22,7 @@ com.axonivy.ivy.ci project-build-plugin - 10.0.3 + ${build.plugin.version} true diff --git a/adobe-esign-connector-demo/src_hd/com/axonivy/connector/adobe/esign/connector/demo/Demo/Demo.xhtml b/adobe-esign-connector-demo/src_hd/com/axonivy/connector/adobe/esign/connector/demo/Demo/Demo.xhtml index 769e0c7..1234016 100644 --- a/adobe-esign-connector-demo/src_hd/com/axonivy/connector/adobe/esign/connector/demo/Demo/Demo.xhtml +++ b/adobe-esign-connector-demo/src_hd/com/axonivy/connector/adobe/esign/connector/demo/Demo/Demo.xhtml @@ -1,88 +1,189 @@ - + - - - Demo - + - -

- Adobe Sign Demos. Please enter a recipient and click one of the buttons to start a demo! -

+ Demo + - - - - + +

+ #{ivy.cms.co('/Dialogs/com/axonivy/connector/adobe/esign/connector/demo/Demo/AdobeSignDemosA')} +

- - - - - - - - - - - + + + + +
+
+
+ +
+
+ + +
+
+
+
- - - - - - - - + +
+
+
+ +
+
+ + + +
+
- - - - - - - - - - - - - - - - +
+
+ +
+
+ + + +
+
+
+
-
-
-
- -
-
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + +
+
- - - +
+
+
+ + +
+
- - +
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ +
+
+
+
+
+
+ +
+ + + + + + +
+
\ No newline at end of file diff --git a/adobe-esign-connector-product/.gitignore b/adobe-esign-connector-product/.gitignore new file mode 100644 index 0000000..c53bcb2 --- /dev/null +++ b/adobe-esign-connector-product/.gitignore @@ -0,0 +1 @@ +openapi.* diff --git a/adobe-esign-connector-product/README.md b/adobe-esign-connector-product/README.md new file mode 100644 index 0000000..3071389 --- /dev/null +++ b/adobe-esign-connector-product/README.md @@ -0,0 +1,135 @@ + +# Adobe Sign Connector + +The Adobe Sign Connector is a project that simplifies the authentication process and enables easy integration and use of Adobe Sign services for signing documents. + +[Adobe Sign](https://www.adobe.com/sign.html) + +An Adobe Sign account needs to be created to setup and use the connector. + +## Adobe Sign account creation + + 1. Create a **AdobeSign** Company **Account** OR for Developer Account creation use [Create Developer Account, APIs for custom applications | Acrobat Sign](https://www.adobe.com/sign/developer-form.html) and follow the steps: + + a. Fill form with personal and business data + continue + ![fill-account-info](images/createAccountFillInfo.png) + + b. Provide a password + continue + ![fill-password](images/createAccountPassword.png) + + c. Provide date of birth + continue + ![fill-birth-date](images/createAccountBirthDate.png) + + d. You will receive a verification code. Insert the code. Will automatically continue + ![verification-code](images/createAccountVerificationCode.png) + + e. Developer account is created + ![account-finished](images/createAccountFinished.png) + +## Demo + +The demo project can be used to test the authentication and signing and the demo implementation can be used as inspiration for development. + +### How to sign a document in the Demo application +:exclamation: The demo will work only after correct setup of the connector + ![demo1](images/demo1.png) + + 1. Choose and upload a PDF file + 2. Fill email of 1 or 2 signers + 3. Create Agreement for 1 or 2 signers + 4. Sign as signer 1 or 2 + 5. A modal dialog with Adobe signing page is opened. Create a signature in the document. + ![demo1](images/demo2.png) + + 6. Confirm the signature with **Click to Sign** + + ![demo1](images/demo3.png) + + 7. The signed document can be downloaded + + ![demo1](images/demo4.png) + + +## Setup + +Adobe Sign provides 2 options for authentication. (See Setup section) + + 1. Integration Key + 2. OAuth2 + + +To setup and use the Adobe Sign Connector it needs to be connected with Adobe. An Adobe administration account needs to be created. (See Adobe Sign account creation) + +### Admin Setup Page +Adobe Sign Connector provides a setup page for easy setup of the connector and setup of the authentization. +To be able to open the Admin Setup page the admin user needs to own `ADOBE_ESIGN_ADMIN` role which is part of the connector. +![admin-page](images/adminPage.png) +#### General +| Variable name | Description | +|--|--| +| adobe-sign-connector.host | hostname of Adobe Sign server | +| adobe-sign-connector.returnPage | relative part of URL that is called after singing was finished | + +#### Integration Key +:exclamation: If Integration Key is set then OAuth is disabled for the connector. If want to use OAuth for the connector then leave the Integration Key empty :exclamation: +| Variable name | Description | +|--|--| +| adobe-sign-connector.integrationKey | Integration key from Adobe Sign configuration | + +##### How to get Integration Key + + 1. Go to your Adobe Sign account page: https://secure.adobesign.com/account/ + 2. Open **Access Tokens** configuration + ![access-tokens](images/integrationKey1.png) + 3. Create new Integration Key + ![create-integration-key](images/integrationKey2.png) + 4. Copy the Integration Key to the Admin Setup Page + ![copy-integration-key](images/integrationKey3.png) + +### Oauth +Adobe API doc references for OAuth + + 1. https://secure.adobesign.com/public/static/oauthDoc.jsp + 2. https://opensource.adobe.com/acrobat-sign/developer_guide/oauth.html + +#### OAuth API Application setup +An API Application needs to be setup at Adobe Sign admin account before OAuth can be configured in the connector. + 1. Go to your Adobe Sign account page: https://secure.adobesign.com/account/ + 2. Go to **API Applications** configuration + ![api-applications](images/oauth1.png) + 3. Create new API Application. Set the Name, Display Name and Domain + ![new-application](images/oauth2.png) + 4. Open the newly created Application and copy ID and Secret to the connector's Admin Setup page + a. Application ID = `adobe-sign-connector.clientId` + b. Client Secret = `adobe-sign-connector.clientSecret` + ![application-detail](images/oauth3.png) +5. Open **Configure OAuth for Application** for your application + a. Copy **Redirect URI** from connector's Admin Setup Page and paste it to the Application Configuration + b. Enable persmissions that can be requested from this Application + ![application-oauth](images/oauth4.png) + + +#### Variables and Admin Setup page for OAuth description +| Variable name | Description | Example +|--|--|--| +| adobe-sign-connector.baseUri | Base URI for getting the access and refresh access tokens (without the `/token` or `/refresh` part) | `https://api.eu2.adobesign.com/oauth/v2` +| adobe-sign-connector.authenticationUri| URL for the Authorization request (:exclamation:differs from tokens URL)| `https://secure.eu2.adobesign.com/public/oauth/v2` +| adobe-sign-connector.clientId| Adobe API Application Client ID| +| adobe-sign-connector.clientSecret| Adobe API Application Client Secret | +| adobe-sign-connector.permissions | List of permissions that will be requested for the OAuth token | `user_read:account user_write:account user_login:account agreement_read:account agreement_write:account agreement_send:account widget_read:account widget_write:account library_read:account library_write:account workflow_read:account workflow_write:account` +| adobe-sign-connector.oauthToken | Info about the OAuth refresh token. Empty means there is no token initialized. To request a new token use the `Save and Request new Token` button | +| adobe-sign-connector.accessToken| Info about the OAuth access token. | +| Redirect URI | This URI just needs to be setup to the API Application at Adobe Sign account page. (see **OAuth API Application setup** section)| `https://localhost:8444/designer/pro/adobe-esign-connector/18A83631DA63DA93/oauthResume.ivp` + + +#### Requesting OAuth token +:exclamation::exclamation::exclamation: Please configure all the Variables in OAuth section on the Admin Setup page (see previous section) as they are necessary for requesting the token. + + 1. Click the `Save and Request new Token` button. You will be redirected to Adobe Sign login page if the configuration of the Variables is correct. +![save-and-request-token](images/tokenRequest1.png) +2. Login with your Adobe Sign account +![adobe-login](images/tokenRequest2.png) +3. After successful login you should see all the requested permissions. Click **Allow Access**. +![request-permissions](images/tokenRequest3.png) +4. The token will be retrieved and you should be redirected back to the connector's Admin Setup page and should be able to see the initialized token. +![token](images/tokenRequest4.png) diff --git a/adobe-esign-connector-product/images/adminPage.png b/adobe-esign-connector-product/images/adminPage.png new file mode 100644 index 0000000..4131954 Binary files /dev/null and b/adobe-esign-connector-product/images/adminPage.png differ diff --git a/adobe-esign-connector-product/images/createAccountBirthDate.png b/adobe-esign-connector-product/images/createAccountBirthDate.png new file mode 100644 index 0000000..0ec7e04 Binary files /dev/null and b/adobe-esign-connector-product/images/createAccountBirthDate.png differ diff --git a/adobe-esign-connector-product/images/createAccountFillInfo.png b/adobe-esign-connector-product/images/createAccountFillInfo.png new file mode 100644 index 0000000..b9b4b3e Binary files /dev/null and b/adobe-esign-connector-product/images/createAccountFillInfo.png differ diff --git a/adobe-esign-connector-product/images/createAccountFinished.png b/adobe-esign-connector-product/images/createAccountFinished.png new file mode 100644 index 0000000..49c0f0d Binary files /dev/null and b/adobe-esign-connector-product/images/createAccountFinished.png differ diff --git a/adobe-esign-connector-product/images/createAccountPassword.png b/adobe-esign-connector-product/images/createAccountPassword.png new file mode 100644 index 0000000..91aabd8 Binary files /dev/null and b/adobe-esign-connector-product/images/createAccountPassword.png differ diff --git a/adobe-esign-connector-product/images/createAccountVerificationCode.png b/adobe-esign-connector-product/images/createAccountVerificationCode.png new file mode 100644 index 0000000..d4f2d9f Binary files /dev/null and b/adobe-esign-connector-product/images/createAccountVerificationCode.png differ diff --git a/adobe-esign-connector-product/images/demo1.png b/adobe-esign-connector-product/images/demo1.png new file mode 100644 index 0000000..994a979 Binary files /dev/null and b/adobe-esign-connector-product/images/demo1.png differ diff --git a/adobe-esign-connector-product/images/demo2.png b/adobe-esign-connector-product/images/demo2.png new file mode 100644 index 0000000..93a19de Binary files /dev/null and b/adobe-esign-connector-product/images/demo2.png differ diff --git a/adobe-esign-connector-product/images/demo3.png b/adobe-esign-connector-product/images/demo3.png new file mode 100644 index 0000000..8a7b26f Binary files /dev/null and b/adobe-esign-connector-product/images/demo3.png differ diff --git a/adobe-esign-connector-product/images/demo4.png b/adobe-esign-connector-product/images/demo4.png new file mode 100644 index 0000000..e49ae20 Binary files /dev/null and b/adobe-esign-connector-product/images/demo4.png differ diff --git a/adobe-esign-connector-product/images/integrationKey1.png b/adobe-esign-connector-product/images/integrationKey1.png new file mode 100644 index 0000000..62da6af Binary files /dev/null and b/adobe-esign-connector-product/images/integrationKey1.png differ diff --git a/adobe-esign-connector-product/images/integrationKey2.png b/adobe-esign-connector-product/images/integrationKey2.png new file mode 100644 index 0000000..2a996cf Binary files /dev/null and b/adobe-esign-connector-product/images/integrationKey2.png differ diff --git a/adobe-esign-connector-product/images/integrationKey3.png b/adobe-esign-connector-product/images/integrationKey3.png new file mode 100644 index 0000000..62e4a8e Binary files /dev/null and b/adobe-esign-connector-product/images/integrationKey3.png differ diff --git a/adobe-esign-connector-product/images/oauth1.png b/adobe-esign-connector-product/images/oauth1.png new file mode 100644 index 0000000..1202bd7 Binary files /dev/null and b/adobe-esign-connector-product/images/oauth1.png differ diff --git a/adobe-esign-connector-product/images/oauth2.png b/adobe-esign-connector-product/images/oauth2.png new file mode 100644 index 0000000..4946080 Binary files /dev/null and b/adobe-esign-connector-product/images/oauth2.png differ diff --git a/adobe-esign-connector-product/images/oauth3.png b/adobe-esign-connector-product/images/oauth3.png new file mode 100644 index 0000000..071821f Binary files /dev/null and b/adobe-esign-connector-product/images/oauth3.png differ diff --git a/adobe-esign-connector-product/images/oauth4.png b/adobe-esign-connector-product/images/oauth4.png new file mode 100644 index 0000000..824c8e3 Binary files /dev/null and b/adobe-esign-connector-product/images/oauth4.png differ diff --git a/adobe-esign-connector-product/images/tokenRequest1.png b/adobe-esign-connector-product/images/tokenRequest1.png new file mode 100644 index 0000000..963817b Binary files /dev/null and b/adobe-esign-connector-product/images/tokenRequest1.png differ diff --git a/adobe-esign-connector-product/images/tokenRequest2.png b/adobe-esign-connector-product/images/tokenRequest2.png new file mode 100644 index 0000000..b5bca73 Binary files /dev/null and b/adobe-esign-connector-product/images/tokenRequest2.png differ diff --git a/adobe-esign-connector-product/images/tokenRequest3.png b/adobe-esign-connector-product/images/tokenRequest3.png new file mode 100644 index 0000000..b65e05d Binary files /dev/null and b/adobe-esign-connector-product/images/tokenRequest3.png differ diff --git a/adobe-esign-connector-product/images/tokenRequest4.png b/adobe-esign-connector-product/images/tokenRequest4.png new file mode 100644 index 0000000..b2f2af1 Binary files /dev/null and b/adobe-esign-connector-product/images/tokenRequest4.png differ diff --git a/MY-PRODUCT-NAME-product/pom.xml b/adobe-esign-connector-product/pom.xml similarity index 73% rename from MY-PRODUCT-NAME-product/pom.xml rename to adobe-esign-connector-product/pom.xml index dd59011..eda1f8c 100644 --- a/MY-PRODUCT-NAME-product/pom.xml +++ b/adobe-esign-connector-product/pom.xml @@ -1,14 +1,9 @@ 4.0.0 - com.axonivy.market - MY-PRODUCT-NAME-product - 10.0.0-SNAPSHOT + com.axonivy.connector.adobe.esign + adobe-esign-connector-product + 10.0.11-SNAPSHOT pom - - - ../MY-PRODUCT-NAME/config/variables.yaml - - @@ -37,14 +32,10 @@ generate-sources - ${skip-readme} - - - diff --git a/MY-PRODUCT-NAME-product/product.json b/adobe-esign-connector-product/product.json similarity index 56% rename from MY-PRODUCT-NAME-product/product.json rename to adobe-esign-connector-product/product.json index bc3625b..fbc60b4 100644 --- a/MY-PRODUCT-NAME-product/product.json +++ b/adobe-esign-connector-product/product.json @@ -1,12 +1,12 @@ { "installers": [ { - "id": "maven-import", + "id": "maven-dependency", "data": { - "projects": [ + "dependencies": [ { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME-demo", + "groupId": "com.axonivy.connector.adobe.esign", + "artifactId": "adobe-esign-connector", "version": "${version}", "type": "iar" } @@ -23,12 +23,12 @@ } }, { - "id": "maven-dependency", + "id": "maven-import", "data": { - "dependencies": [ + "projects": [ { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME", + "groupId": "com.axonivy.connector.adobe.esign", + "artifactId": "adobe-esign-connector", "version": "${version}", "type": "iar" } @@ -43,27 +43,6 @@ } ] } - }, - { - "id": "maven-dropins", - "data": { - "dependencies": [ - { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME", - "version": "${version}" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } } ] } diff --git a/MY-PRODUCT-NAME-product/zip.xml b/adobe-esign-connector-product/zip.xml similarity index 77% rename from MY-PRODUCT-NAME-product/zip.xml rename to adobe-esign-connector-product/zip.xml index 003f06c..7a40a2d 100644 --- a/MY-PRODUCT-NAME-product/zip.xml +++ b/adobe-esign-connector-product/zip.xml @@ -11,15 +11,9 @@ . product.json - openapi.json - **/*.png - - - - target - / - + openapi.* README.md + **/*.png diff --git a/adobe-esign-connector-test/.classpath b/adobe-esign-connector-test/.classpath new file mode 100644 index 0000000..e938886 --- /dev/null +++ b/adobe-esign-connector-test/.classpath @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/adobe-esign-connector-test/.gitignore b/adobe-esign-connector-test/.gitignore new file mode 100644 index 0000000..9b0d458 --- /dev/null +++ b/adobe-esign-connector-test/.gitignore @@ -0,0 +1,19 @@ +# general +Thumbs.db +.DS_Store +*~ +*.log + +# java +*.class +hs_err_pid* + +# maven +target/ +lib/mvn-deps/ + +# ivy +classes/ +src_dataClasses/ +src_wsproc/ +logs/ diff --git a/adobe-esign-connector-test/.project b/adobe-esign-connector-test/.project new file mode 100644 index 0000000..364fb0a --- /dev/null +++ b/adobe-esign-connector-test/.project @@ -0,0 +1,49 @@ + + + adobe-esign-connector-test + + + + + + ch.ivyteam.ivy.designer.dataClasses.ui.ivyDataClassBuilder + + + + + ch.ivyteam.ivy.designer.process.ui.ivyWebServiceProcessClassBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + ch.ivyteam.ivy.designer.ide.ivyModelValidationBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + ch.ivyteam.ivy.project.IvyProjectNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.jem.beaninfo.BeanInfoNature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/adobe-esign-connector-test/.settings/.jsdtscope b/adobe-esign-connector-test/.settings/.jsdtscope new file mode 100644 index 0000000..869c01d --- /dev/null +++ b/adobe-esign-connector-test/.settings/.jsdtscope @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/adobe-esign-connector-test/.settings/ch.ivyteam.ivy.designer.prefs b/adobe-esign-connector-test/.settings/ch.ivyteam.ivy.designer.prefs new file mode 100644 index 0000000..d7b0ff4 --- /dev/null +++ b/adobe-esign-connector-test/.settings/ch.ivyteam.ivy.designer.prefs @@ -0,0 +1,5 @@ +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.connector.adobe.esign.test.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.connector.adobe.esign.test +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 +eclipse.preferences.version=1 diff --git a/adobe-esign-connector-test/.settings/org.eclipse.jdt.core.prefs b/adobe-esign-connector-test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..d4540a5 --- /dev/null +++ b/adobe-esign-connector-test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,10 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/adobe-esign-connector-test/.settings/org.eclipse.wst.common.component b/adobe-esign-connector-test/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..0a27a52 --- /dev/null +++ b/adobe-esign-connector-test/.settings/org.eclipse.wst.common.component @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/adobe-esign-connector-test/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml b/adobe-esign-connector-test/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml new file mode 100644 index 0000000..9b4b9fc --- /dev/null +++ b/adobe-esign-connector-test/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/adobe-esign-connector-test/.settings/org.eclipse.wst.common.project.facet.core.xml b/adobe-esign-connector-test/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..156ecdb --- /dev/null +++ b/adobe-esign-connector-test/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/adobe-esign-connector-test/.settings/org.eclipse.wst.css.core.prefs b/adobe-esign-connector-test/.settings/org.eclipse.wst.css.core.prefs new file mode 100644 index 0000000..5ddc6bd --- /dev/null +++ b/adobe-esign-connector-test/.settings/org.eclipse.wst.css.core.prefs @@ -0,0 +1,2 @@ +css-profile/=org.eclipse.wst.css.core.cssprofile.css3 +eclipse.preferences.version=1 diff --git a/adobe-esign-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.container b/adobe-esign-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/adobe-esign-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/adobe-esign-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.name b/adobe-esign-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/adobe-esign-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/adobe-esign-connector-test/cms/Files/samplePdf.pdf b/adobe-esign-connector-test/cms/Files/samplePdf.pdf new file mode 100644 index 0000000..c0e31a0 Binary files /dev/null and b/adobe-esign-connector-test/cms/Files/samplePdf.pdf differ diff --git a/adobe-esign-connector-test/config/custom-fields.yaml b/adobe-esign-connector-test/config/custom-fields.yaml new file mode 100644 index 0000000..aa19ae0 --- /dev/null +++ b/adobe-esign-connector-test/config/custom-fields.yaml @@ -0,0 +1,20 @@ +# == Custom Fields Information == +# +# You can define here your project custom fields. +# Have a look at our documentation for more information. +# +CustomFields: +# Tasks: +# MyTaskCustomField: +# Label: My task custom field +# Description: This new task custom field can be used to ... +# Type: STRING +# Cases: +# MyCaseCustomField: +# Label: My case custom field +# Description: This new case custom field can be used to ... +# Type: STRING +# Starts: +# MyStartCustomField: +# Label: My start custom field +# Description: This new start custom field can be used to ... diff --git a/adobe-esign-connector-test/config/databases.yaml b/adobe-esign-connector-test/config/databases.yaml new file mode 100644 index 0000000..247b128 --- /dev/null +++ b/adobe-esign-connector-test/config/databases.yaml @@ -0,0 +1 @@ +Databases: diff --git a/adobe-esign-connector-test/config/overrides.any b/adobe-esign-connector-test/config/overrides.any new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/adobe-esign-connector-test/config/overrides.any @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/adobe-esign-connector-test/config/persistence.xml b/adobe-esign-connector-test/config/persistence.xml new file mode 100644 index 0000000..d6b96d7 --- /dev/null +++ b/adobe-esign-connector-test/config/persistence.xml @@ -0,0 +1,2 @@ + + diff --git a/adobe-esign-connector-test/config/rest-clients.yaml b/adobe-esign-connector-test/config/rest-clients.yaml new file mode 100644 index 0000000..8e85296 --- /dev/null +++ b/adobe-esign-connector-test/config/rest-clients.yaml @@ -0,0 +1 @@ +RestClients: diff --git a/adobe-esign-connector-test/config/roles.xml b/adobe-esign-connector-test/config/roles.xml new file mode 100644 index 0000000..59892fe --- /dev/null +++ b/adobe-esign-connector-test/config/roles.xml @@ -0,0 +1,4 @@ + + + Everybody + diff --git a/adobe-esign-connector-test/config/users.xml b/adobe-esign-connector-test/config/users.xml new file mode 100644 index 0000000..51a6906 --- /dev/null +++ b/adobe-esign-connector-test/config/users.xml @@ -0,0 +1,2 @@ + + diff --git a/adobe-esign-connector-test/config/variables.yaml b/adobe-esign-connector-test/config/variables.yaml new file mode 100644 index 0000000..64c8fa0 --- /dev/null +++ b/adobe-esign-connector-test/config/variables.yaml @@ -0,0 +1,9 @@ +# == Variables == +# +# You can define here your project Variables. +# If you want to define/override a Variable for a specific Environment, +# add an additional ‘variables.yaml’ file in a subdirectory in the ‘Config’ folder: +# '/Config/_/variables.yaml +# +Variables: +# myVariable: value diff --git a/adobe-esign-connector-test/config/webservice-clients.yaml b/adobe-esign-connector-test/config/webservice-clients.yaml new file mode 100644 index 0000000..060b018 --- /dev/null +++ b/adobe-esign-connector-test/config/webservice-clients.yaml @@ -0,0 +1 @@ +WebServiceClients: diff --git a/adobe-esign-connector-test/dataclasses/com/axonivy/connector/adobe/esign/test/Data.ivyClass b/adobe-esign-connector-test/dataclasses/com/axonivy/connector/adobe/esign/test/Data.ivyClass new file mode 100644 index 0000000..13fd8e1 --- /dev/null +++ b/adobe-esign-connector-test/dataclasses/com/axonivy/connector/adobe/esign/test/Data.ivyClass @@ -0,0 +1,2 @@ +Data #class +com.axonivy.connector.adobe.esign.test #namespace diff --git a/adobe-esign-connector-test/pom.xml b/adobe-esign-connector-test/pom.xml new file mode 100644 index 0000000..d11dc75 --- /dev/null +++ b/adobe-esign-connector-test/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + com.axonivy.connector.adobe.esign + adobe-esign-connector-test + 10.0.11-SNAPSHOT + iar + + + com.axonivy.connector.adobe.esign + adobe-esign-connector + 10.0.11-SNAPSHOT + iar + + + com.axonivy.ivy.test + unit-tester + 10.0.0 + test + + + + src_test + + + com.axonivy.ivy.ci + project-build-plugin + 10.0.6 + true + + + + diff --git a/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/AdobeSignAgreementsServiceMock.java b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/AdobeSignAgreementsServiceMock.java new file mode 100644 index 0000000..9dccee9 --- /dev/null +++ b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/AdobeSignAgreementsServiceMock.java @@ -0,0 +1,68 @@ +package com.axonivy.connector.adobe.esign.connector.test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import javax.annotation.security.PermitAll; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.commons.io.IOUtils; + +import ch.ivyteam.api.API; +import io.swagger.v3.oas.annotations.Hidden; + +@Hidden +@PermitAll +@Path("adobeSignMock") +public class AdobeSignAgreementsServiceMock { + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("agreements") + public Response createAgreement(String payload) { + API.checkParameterNotNull(payload, "payload"); + return Response.status(201).entity(load("json/createAgreement.json")).build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("agreements/{agreementId}/documents") + public Response getDocuments(@PathParam(value = "agreementId") String agreementId) { + API.checkParameterNotNull(agreementId, "agreementId"); + return Response.status(201).entity(load("json/getDocuments.json")).build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("agreements/{agreementId}/documents/{documentId}") + public Response downloadDocument(@PathParam(value = "agreementId") String agreementId, + @PathParam(value = "documentId") String documentId) throws IOException { + API.checkParameterNotNull(agreementId, "agreementId"); + return Response.status(200).entity(TestService.getSamplePdf()).build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("agreements/{agreementId}/signingUrls") + public Response getSigningUrls(@PathParam(value = "agreementId") String agreementId) { + API.checkParameterNotNull(agreementId, "agreementId"); + return Response.status(201).entity(load("json/getSigningUrls.json")).build(); + } + + private static String load(String path) { + try (InputStream is = AdobeSignAgreementsServiceMock.class.getResourceAsStream(path)) { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException ex) { + throw new RuntimeException("Failed to read resource: " + path); + } + } +} diff --git a/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/AdobeSignTransientDocumentsServiceMock.java b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/AdobeSignTransientDocumentsServiceMock.java new file mode 100644 index 0000000..9fcb013 --- /dev/null +++ b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/AdobeSignTransientDocumentsServiceMock.java @@ -0,0 +1,43 @@ +package com.axonivy.connector.adobe.esign.connector.test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import javax.annotation.security.PermitAll; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.commons.io.IOUtils; + +import ch.ivyteam.api.API; +import io.swagger.v3.oas.annotations.Hidden; + +@Hidden +@PermitAll +@Path("adobeSignMock") +public class AdobeSignTransientDocumentsServiceMock { + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("transientDocuments") + public Response transientDocuemts(String payload) { + API.checkParameterNotNull(payload, "payload"); + return Response.status(201) + .entity(load("json/uploadDocument.json")) + .build(); + } + + private static String load(String path) { + try (InputStream is = AdobeSignTransientDocumentsServiceMock.class.getResourceAsStream(path)) { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException ex) { + throw new RuntimeException("Failed to read resource: " + path); + } + } +} diff --git a/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/TestService.java b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/TestService.java new file mode 100644 index 0000000..f1ac386 --- /dev/null +++ b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/TestService.java @@ -0,0 +1,27 @@ +package com.axonivy.connector.adobe.esign.connector.test; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Locale; + +import org.apache.commons.compress.utils.IOUtils; + +import ch.ivyteam.ivy.cm.ContentObjectValue; +import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.ivy.scripting.objects.File; + +public class TestService { + private static final String SAMPLE_PDF_CMS_PATH = "/Files/samplePdf"; + + public static java.io.File getSamplePdf() throws IOException { + ContentObjectValue cov = Ivy.cm().findValue(SAMPLE_PDF_CMS_PATH).resolve(Locale.ENGLISH).orElse(null); + if (cov == null) { + return null; + } + java.io.File sampleFile = new File("samplePdf.pdf", true).getJavaFile(); + FileOutputStream fos = new FileOutputStream(sampleFile); + IOUtils.copy(cov.read().inputStream(), fos); + return sampleFile; + + } +} diff --git a/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/createAgreement.json b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/createAgreement.json new file mode 100644 index 0000000..31c3d25 --- /dev/null +++ b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/createAgreement.json @@ -0,0 +1,3 @@ +{ + "id": "CBJCHBCAABAAMaMFFCyiTyCblablablablabla" +} diff --git a/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/getDocuments.json b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/getDocuments.json new file mode 100644 index 0000000..9cecbad --- /dev/null +++ b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/getDocuments.json @@ -0,0 +1,17 @@ +{ + "documents": [ + { + "createdDate": [ + 2023, + 9, + 18 + ], + "id": "3AAABLblqZhDM2f8YEhDF1N6Oz-8ZY5jVZVgpRh2b_69hblablablablabla", + "label": null, + "mimeType": "application/pdf", + "name": "sample.pdf", + "numPages": 2 + } + ], + "supportingDocuments": null +} diff --git a/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/getSigningUrls.json b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/getSigningUrls.json new file mode 100644 index 0000000..3dfc4d3 --- /dev/null +++ b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/getSigningUrls.json @@ -0,0 +1,13 @@ +{ + "signingUrlSetInfos": [ + { + "signingUrlSetName": null, + "signingUrls": [ + { + "email": "stma@mailinator.com", + "esignUrl": "https://secure.eu2.adobesign.com/public/apiesign?pid=CBFCIBAA3AAABLblqZhBfFw-mWHXrt1tSN_AfnQQld3Ea9pq61iY57nLp7Duyp5I8XoIyLkT1TcjoW7lshFZXF4zoa5UPIOBRKUPhP8YuenqrWRNjVAXoTO8P7eMr3qIr8-hOhCOIfKUXiB0UOmQ%2A&client_id=CBJCHblablablablabla" + } + ] + } + ] +} diff --git a/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/uploadDocument.json b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/uploadDocument.json new file mode 100644 index 0000000..a8c68b8 --- /dev/null +++ b/adobe-esign-connector-test/src/com/axonivy/connector/adobe/esign/connector/test/json/uploadDocument.json @@ -0,0 +1,3 @@ +{ + "transientDocumentId": "3AAABLblqZhDVSvtTTuHIxFoTcvrHMhZwdRBEz62SCvv1G8yTaPzxcu8NGkq9IIILoVyC4UHJ8vgl_Mw5CY9lBSMEbMriLGzZyy6KCWpF8Ac2TpP-3Bpfrr8LHyv92ukAsKnyJBDP9XPpSyVOm1CtLUbNtX7M6cfu_LKRSTl-nGfMK6eRalZ6AsxLzDTW8lALl20Md5x5EyOkBLW1ZvvyF2OITH-cPVgBAoh7ejVkDiHwskPq9NIUoTZ4jN3bvq0ma5Dz7gP1F5RGuLlzGDlje3_MIof7-EgdIQt92grPt7fe0-Dfgjblablabla" +} diff --git a/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestAdobeSignConnector.java b/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestAdobeSignConnector.java new file mode 100644 index 0000000..711d10d --- /dev/null +++ b/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestAdobeSignConnector.java @@ -0,0 +1,34 @@ +package com.axonivy.connector.adobe.esign.connector.test; + +import ch.ivyteam.ivy.application.IApplication; +import ch.ivyteam.ivy.bpm.exec.client.IvyProcessTest; +import ch.ivyteam.ivy.environment.AppFixture; +import ch.ivyteam.ivy.rest.client.RestClient; +import ch.ivyteam.ivy.rest.client.RestClient.Builder; +import ch.ivyteam.ivy.rest.client.RestClients; + +@IvyProcessTest +public class TestAdobeSignConnector { + protected static final String TRANSIENT_DOCUMENTS = "TransientDocuments"; + protected static final String AGREEMENTS = "Agreements"; + + protected void prepareRestClient(IApplication app, AppFixture fixture, String clientName) { + fixture.var("adobe-sign-connector.host", "TESTHOST"); + fixture.var("adobe-sign-connector.integrationKey", "TESTUSER"); + RestClient restClient = RestClients.of(app).find(clientName); + // change created client: use test url and a slightly different version of + // the + // DocuWare Auth feature + Builder builder = RestClient.create(restClient.name()).uuid(restClient.uniqueId()) + .uri("http://{ivy.engine.host}:{ivy.engine.http.port}/{ivy.request.application}/api/adobeSignMock") + .description(restClient.description()).properties(restClient.properties()); + + for (String feature : restClient.features()) { + builder.feature(feature); + } + + builder.feature("ch.ivyteam.ivy.rest.client.security.CsrfHeaderFeature"); + restClient = builder.toRestClient(); + RestClients.of(app).set(restClient); + } +} diff --git a/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestAgreementsService.java b/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestAgreementsService.java new file mode 100644 index 0000000..157a4e9 --- /dev/null +++ b/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestAgreementsService.java @@ -0,0 +1,143 @@ +package com.axonivy.connector.adobe.esign.connector.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.axonivy.connector.adobe.esign.connector.AgreementsData; +import com.axonivy.connector.adobe.esign.connector.rest.DownloadResult; +import com.axonivy.connector.adobe.esign.connector.service.AdobeSignService; + +import api.rest.v6.client.AgreementCreationInfo; +import api.rest.v6.client.AgreementCreationInfo.SignatureTypeEnum; +import api.rest.v6.client.AgreementCreationInfo.StateEnum; +import api.rest.v6.client.AgreementCreationInfoParticipantSetsInfo.RoleEnum; +import api.rest.v6.client.AgreementCreationResponse; +import api.rest.v6.client.AgreementDocuments; +import api.rest.v6.client.SigningUrlResponseSigningUrlSetInfos; +import ch.ivyteam.ivy.application.IApplication; +import ch.ivyteam.ivy.bpm.engine.client.BpmClient; +import ch.ivyteam.ivy.bpm.engine.client.ExecutionResult; +import ch.ivyteam.ivy.bpm.engine.client.element.BpmElement; +import ch.ivyteam.ivy.bpm.engine.client.element.BpmProcess; +import ch.ivyteam.ivy.bpm.exec.client.IvyProcessTest; +import ch.ivyteam.ivy.environment.AppFixture; +import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.ivy.security.ISession; + +@IvyProcessTest +public class TestAgreementsService extends TestAdobeSignConnector { + + private static final BpmElement testeeCreateAgreement = BpmProcess.path("connector/Agreements") + .elementName("createAgreement(AgreementCreationInfo)"); + + private static final BpmElement testeeGetDocuments = BpmProcess.path("connector/Agreements") + .elementName("getDocuments(String)"); + + private static final BpmElement testeeDownloadDocument = BpmProcess.path("connector/Agreements") + .elementName("dowloadDocument(String, String, String, Boolean)"); + + private static final BpmElement testeeGetSigningUrls = BpmProcess.path("connector/Agreements") + .elementName("getSigningURLs(String,String)"); + + @Test + public void createAgreement(BpmClient bpmClient, ISession session, AppFixture fixture, IApplication app) + throws IOException { + + prepareRestClient(app, fixture, AGREEMENTS); + + AgreementCreationInfo agreement = createTestAgreement(); + + ExecutionResult result = bpmClient.start().subProcess(testeeCreateAgreement).withParam("agreement", agreement) + .execute(); + AgreementsData data = result.data().last(); + AgreementCreationResponse response = data.getAgreementCreationResponse(); + assertThat(response).isNotNull(); + assertThat(response.getId()).isNotEmpty(); + } + + @Test + public void getDocuments(BpmClient bpmClient, ISession session, AppFixture fixture, IApplication app) + throws IOException { + + prepareRestClient(app, fixture, AGREEMENTS); + + String agreementId = "test-agreement-id"; + + ExecutionResult result = bpmClient.start().subProcess(testeeGetDocuments).withParam("agreementId", agreementId) + .execute(); + AgreementsData data = result.data().last(); + AgreementDocuments response = data.getDocuments(); + assertThat(response).isNotNull(); + assertThat(response.getDocuments()).isNotEmpty(); + } + + @Test + public void downloadDocument(BpmClient bpmClient, ISession session, AppFixture fixture, IApplication app) + throws IOException { + + prepareRestClient(app, fixture, AGREEMENTS); + + String agreementId = "test-agreement-id"; + String documentId = "test-document-id"; + String filename = "sample.pdf"; + Boolean asFile = Boolean.TRUE; + + ExecutionResult result = bpmClient.start().subProcess(testeeDownloadDocument) + .withParam("agreementId", agreementId).withParam("documentId", documentId) + .withParam("filename", filename).withParam("asFile", asFile).execute(); + AgreementsData data = result.data().last(); + DownloadResult downloadResult = data.getDownload(); + assertThat(downloadResult).isNotNull(); + if (downloadResult.getError() != null) { + Ivy.log().error(downloadResult.getError()); + } + assertThat(downloadResult.getFile()).isNotNull(); + } + + @Test + public void getSigningUrls(BpmClient bpmClient, ISession session, AppFixture fixture, IApplication app) + throws IOException { + + prepareRestClient(app, fixture, AGREEMENTS); + + String agreementId = "test-agreement-id"; + String frameParent = "test"; + + ExecutionResult result = bpmClient.start().subProcess(testeeGetSigningUrls) + .withParam("agreementId", agreementId).withParam("frameParent", frameParent).execute(); + AgreementsData data = result.data().last(); + + List signingUrls = data.getSigningUrls(); + + assertThat(signingUrls).isNotNull(); + assertThat(signingUrls).isNotEmpty(); + } + + private AgreementCreationInfo createTestAgreement() { + AgreementCreationInfo agreement = new AgreementCreationInfo(); + + agreement.setName("test name"); + agreement.setMessage("Please sign this document!"); + agreement.setSignatureType(SignatureTypeEnum.ESIGN); + agreement.setState(StateEnum.IN_PROCESS); + + // add signers + AdobeSignService.getInstance().createParticipantInfoForEmail(Arrays.asList("testEmail@test.test"), + RoleEnum.SIGNER); + agreement.setParticipantSetsInfo(AdobeSignService.getInstance() + .createParticipantInfoForEmail(Arrays.asList("testEmail@test.test"), RoleEnum.SIGNER)); + + // add documentIds - need to be already transferred with the upload document + // service + agreement.setFileInfos( + AdobeSignService.getInstance().createFileInfosForDocumentIds(Arrays.asList("test-document-id"))); + + agreement.setEmailOption(AdobeSignService.getInstance().createAllDisabledSendOptions()); + return agreement; + } +} diff --git a/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestTransientDocumentsService.java b/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestTransientDocumentsService.java new file mode 100644 index 0000000..a85fa13 --- /dev/null +++ b/adobe-esign-connector-test/src_test/com/axonivy/connector/adobe/esign/connector/test/TestTransientDocumentsService.java @@ -0,0 +1,39 @@ +package com.axonivy.connector.adobe.esign.connector.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.axonivy.connector.adobe.esign.connector.TransientDocumentsData; + +import ch.ivyteam.ivy.application.IApplication; +import ch.ivyteam.ivy.bpm.engine.client.BpmClient; +import ch.ivyteam.ivy.bpm.engine.client.ExecutionResult; +import ch.ivyteam.ivy.bpm.engine.client.element.BpmElement; +import ch.ivyteam.ivy.bpm.engine.client.element.BpmProcess; +import ch.ivyteam.ivy.bpm.exec.client.IvyProcessTest; +import ch.ivyteam.ivy.environment.AppFixture; +import ch.ivyteam.ivy.security.ISession; + +@IvyProcessTest +public class TestTransientDocumentsService extends TestAdobeSignConnector { + + + private static final BpmElement testeeUploadDocument = BpmProcess.path("connector/TransientDocuments") + .elementName("uploadDocument(file)"); + + @Test + public void uploadFile(BpmClient bpmClient, ISession session, AppFixture fixture, IApplication app) + throws IOException { + prepareRestClient(app, fixture, TRANSIENT_DOCUMENTS); + + java.io.File pdf = TestService.getSamplePdf(); + assertThat(pdf).isNotNull(); + + ExecutionResult result = bpmClient.start().subProcess(testeeUploadDocument).withParam("file", pdf).execute(); + TransientDocumentsData data = result.data().last(); + assertThat(data.getId()).isNotEmpty(); + } +} diff --git a/adobe-esign-connector/cms/cms_en.yaml b/adobe-esign-connector/cms/cms_en.yaml new file mode 100644 index 0000000..553466b --- /dev/null +++ b/adobe-esign-connector/cms/cms_en.yaml @@ -0,0 +1,15 @@ +Dialogs: + com: + axonivy: + connector: + adobe: + esign: + connector: + AdminSetup: + General: General + IntegrationKey: Integration key + IntegrationKeyInfo: Setting the integrationKey will disable OAuth. If you wanna use OAuth then fill the OAuth section and leave the integrationKey empty. + OAuth: OAuth + RedirectURI: Redirect URI + RequestToken: Save and Request new Token + Title: Adobe Sign Admin Setup diff --git a/adobe-esign-connector/config/rest-clients.yaml b/adobe-esign-connector/config/rest-clients.yaml index f1871f3..b1c4238 100644 --- a/adobe-esign-connector/config/rest-clients.yaml +++ b/adobe-esign-connector/config/rest-clients.yaml @@ -23,10 +23,21 @@ RestClients: Url: https://{host}/api/rest/v6 Features: - ch.ivyteam.ivy.rest.client.mapper.JsonFeature + - com.axonivy.connector.adobe.esign.connector.auth.OAuth2Feature + Properties: + AUTH.scope: ${ivy.var.adobe-sign-connector.permissions} + AUTH.baseUri: ${ivy.var.adobe-sign-connector.baseUri} + JSON.Serialization.PROPERTY_INCLUSION: NON_NULL + AUTH.clientId: ${ivy.var.adobe-sign-connector.clientId} + AUTH.clientSecret: ${ivy.var.adobe-sign-connector.clientSecret} + AUTH.useAppPermissions: ${ivy.var.adobe-sign-connector.useAppPermissions} + AUTH.useUserPassFlow: ${ivy.var.adobe-sign-connector.useUserPassFlow.enabled} + PATH.host: ${ivy.var.adobe-sign-connector.host} + AUTH.integrationKey: ${ivy.var.adobe-sign-connector.integrationKey} OpenAPI: SpecUrl: file:///C:/Users/jpl/Downloads/json/REST-SDK-V6-master/json/baseUrisV3.json Namespace: api.rest.v6.client - Agreements (secure.na1.adobesign.com): + Agreements: UUID: 8e96fab1-4701-47cb-ae35-d821bb12305b Url: https://{host}/api/rest/v6 Features: @@ -36,35 +47,31 @@ RestClients: AUTH.scope: ${ivy.var.adobe-sign-connector.permissions} AUTH.baseUri: ${ivy.var.adobe-sign-connector.baseUri} JSON.Serialization.PROPERTY_INCLUSION: NON_NULL - AUTH.appId: ${ivy.var.adobe-sign-connector.appId} - AUTH.secretKey: ${ivy.var.adobe-sign-connector.secretKey} + AUTH.clientId: ${ivy.var.adobe-sign-connector.clientId} + AUTH.clientSecret: ${ivy.var.adobe-sign-connector.clientSecret} AUTH.useAppPermissions: ${ivy.var.adobe-sign-connector.useAppPermissions} AUTH.useUserPassFlow: ${ivy.var.adobe-sign-connector.useUserPassFlow.enabled} - AUTH.user: ${ivy.var.adobe-sign-connector.useUserPassFlow.user} - AUTH.password: ${ivy.var.adobe-sign-connector.useUserPassFlow.pass} PATH.host: ${ivy.var.adobe-sign-connector.host} AUTH.integrationKey: ${ivy.var.adobe-sign-connector.integrationKey} OpenAPI: SpecUrl: file:///C:/Users/jpl/OneDrive%20-%20Docuware%20GmbH/data/Projekte/Ricoh/HR%20Transformation/Adobe%20Sign/agreementsV2.json Namespace: api.rest.v6.client ResolveFully: true - TransientDocuments (secure.eu2.echosign.com): + TransientDocuments: UUID: 6a243983-5f6d-4278-89ec-aab09fea161c Url: https://{host}/api/rest/v6 Features: - - ch.ivyteam.ivy.rest.client.mapper.JsonFeature - org.glassfish.jersey.media.multipart.MultiPartFeature - com.axonivy.connector.adobe.esign.connector.auth.OAuth2Feature + - ch.ivyteam.ivy.rest.client.mapper.JsonFeature Properties: AUTH.scope: ${ivy.var.adobe-sign-connector.permissions} AUTH.baseUri: ${ivy.var.adobe-sign-connector.baseUri} JSON.Serialization.PROPERTY_INCLUSION: NON_NULL - AUTH.appId: ${ivy.var.adobe-sign-connector.appId} - AUTH.secretKey: ${ivy.var.adobe-sign-connector.secretKey} + AUTH.clientId: ${ivy.var.adobe-sign-connector.clientId} + AUTH.clientSecret: ${ivy.var.adobe-sign-connector.clientSecret} AUTH.useAppPermissions: ${ivy.var.adobe-sign-connector.useAppPermissions} AUTH.useUserPassFlow: ${ivy.var.adobe-sign-connector.useUserPassFlow.enabled} - AUTH.user: ${ivy.var.adobe-sign-connector.useUserPassFlow.user} - AUTH.password: ${ivy.var.adobe-sign-connector.useUserPassFlow.pass} PATH.host: ${ivy.var.adobe-sign-connector.host} AUTH.integrationKey: ${ivy.var.adobe-sign-connector.integrationKey} OpenAPI: diff --git a/adobe-esign-connector/config/roles.xml b/adobe-esign-connector/config/roles.xml index 59892fe..1990089 100644 --- a/adobe-esign-connector/config/roles.xml +++ b/adobe-esign-connector/config/roles.xml @@ -1,4 +1,7 @@ Everybody + + ADOBE_ESIGN_ADMIN + diff --git a/adobe-esign-connector/config/variables.yaml b/adobe-esign-connector/config/variables.yaml index d9ce3d6..199b449 100644 --- a/adobe-esign-connector/config/variables.yaml +++ b/adobe-esign-connector/config/variables.yaml @@ -1,8 +1,39 @@ # == Variables == -# +# # You can define here your project Variables. -# If you want to define/override a Variable for a specific Environment, -# add an additional ‘variables.yaml’ file in a subdirectory in the ‘Config’ folder: +# If you want to define/override a Variable for a specific Environment, +# add an additional ‘variables.yaml’ file in a subdirectory in the ‘Config’ folder: # '/Config/_/variables.yaml # Variables: + adobe-sign-connector: + # Hostname of Adobe Sign server + host: '' + + # Relative part of URL that is called after singing was finished + returnPage: '' + + # Integration key from Adobe Sign configuration + integrationKey: '' + + # Base URI for getting the access and refresh access tokens (without the `/token` or `/refresh` part) + baseUri: 'https://api.eu2.adobesign.com/oauth/v2' + + # URL for the Authorization request (differs from tokens URL) + authenticationUri: 'https://secure.eu2.adobesign.com/public/oauth/v2' + + # Adobe API Application Client ID + clientId: '' + + # Adobe API Application Client Secret + # [password] + clientSecret: '' + + # List of permissions that will be requested for the OAuth token + permissions: user_read:account user_write:account user_login:account agreement_read:account agreement_write:account agreement_send:account + + # DO NOT MODIFY DIRECTLY, USE ADMIN SETUP PAGE. Info about the OAuth refresh token. Empty means there is no token initialized. To request a new token use the `Save and Request new Token` button + oauthToken: '' + + # DO NOT MODIFY DIRECTLY, USE ADMIN SETUP PAGE. Info about the OAuth access token. + accessToken: '' diff --git a/adobe-esign-connector/dataclasses/com/axonivy/connector/adobe/esign/connector/AuthSetupData.ivyClass b/adobe-esign-connector/dataclasses/com/axonivy/connector/adobe/esign/connector/AuthSetupData.ivyClass new file mode 100644 index 0000000..4e40fbb --- /dev/null +++ b/adobe-esign-connector/dataclasses/com/axonivy/connector/adobe/esign/connector/AuthSetupData.ivyClass @@ -0,0 +1,2 @@ +AuthSetupData #class +com.axonivy.connector.adobe.esign.connector #namespace diff --git a/adobe-esign-connector/pom.xml b/adobe-esign-connector/pom.xml index ebf80db..524f781 100644 --- a/adobe-esign-connector/pom.xml +++ b/adobe-esign-connector/pom.xml @@ -4,18 +4,11 @@ 4.0.0 com.axonivy.connector.adobe.esign adobe-esign-connector - 1.0.0-SNAPSHOT + 10.0.11-SNAPSHOT iar 10.0.6 - - - com.axonivy.connector.msgraph - msgraph-oauth-feature - 10.0.3 - - diff --git a/adobe-esign-connector/processes/Setup/AdminSetup.p.json b/adobe-esign-connector/processes/Setup/AdminSetup.p.json new file mode 100644 index 0000000..d8c572f --- /dev/null +++ b/adobe-esign-connector/processes/Setup/AdminSetup.p.json @@ -0,0 +1,65 @@ +{ + "format" : "10.0.0", + "id" : "18A83631DA63DA93", + "config" : { + "data" : "com.axonivy.connector.adobe.esign.connector.AuthSetupData" + }, + "elements" : [ { + "id" : "f0", + "type" : "RequestStart", + "name" : "start.ivp", + "config" : { + "callSignature" : "start", + "responsible" : "ADOBE_ESIGN_ADMIN", + "outLink" : "start.ivp", + "wfuser" : "1", + "startName" : "Adobe Sign Connector Admin Setup" + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 } + }, + "connect" : { "id" : "f4", "to" : "f3" } + }, { + "id" : "f1", + "type" : "TaskEnd", + "visual" : { + "at" : { "x" : 520, "y" : 64 } + } + }, { + "id" : "f3", + "type" : "DialogCall", + "name" : "AuthSetup", + "config" : { + "dialogId" : "com.axonivy.connector.adobe.esign.connector.AdminSetup", + "startMethod" : "start()" + }, + "visual" : { + "at" : { "x" : 224, "y" : 64 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + }, { + "id" : "f5", + "type" : "RequestStart", + "name" : "oauthResume.ivp", + "config" : { + "callSignature" : "oauthResume", + "responsible" : "ADOBE_ESIGN_ADMIN", + "outLink" : "oauthResume.ivp", + "showInStartList" : false, + "input" : { + "params" : [ + { "name" : "code", "type" : "String" } + ], + "code" : [ + "import com.axonivy.connector.adobe.esign.connector.service.AdminSetupService;", + "AdminSetupService.getNewAccessToken(param.code);" + ] + }, + "wfuser" : "1" + }, + "visual" : { + "at" : { "x" : 96, "y" : 200 } + }, + "connect" : { "id" : "f9", "to" : "f3", "via" : [ { "x" : 224, "y" : 200 } ] } + } ] +} \ No newline at end of file diff --git a/adobe-esign-connector/processes/connector/BaseUris.p.json b/adobe-esign-connector/processes/connector/BaseUris.p.json index 109e66d..b7d12c8 100644 --- a/adobe-esign-connector/processes/connector/BaseUris.p.json +++ b/adobe-esign-connector/processes/connector/BaseUris.p.json @@ -8,14 +8,14 @@ "elements" : [ { "id" : "f0", "type" : "CallSubStart", - "name" : "call()", + "name" : "get()", "config" : { - "callSignature" : "call" + "callSignature" : "get" }, "visual" : { "at" : { "x" : 96, "y" : 64 } }, - "connect" : { "id" : "f4", "to" : "f3" } + "connect" : { "id" : "f6", "to" : "f5" } }, { "id" : "f1", "type" : "CallSubEnd", @@ -23,26 +23,19 @@ "at" : { "x" : 352, "y" : 64 } } }, { - "id" : "f3", + "id" : "f5", "type" : "RestClientCall", + "name" : "BaseUris", "config" : { "path" : "/baseUris", - "headers" : { - "Accept" : "*/*", - "Authorization" : "\"Bearer 3AAABLblqZhAzw3xeMAg0OJ_aouHskZvurSIreo4VCqw-Iqla6aYwav8m1vTm5AHGOXndztXnqUni_wxNMTM83Rdr-qxUrhe9\"" - }, "clientId" : "9774590a-7adb-4910-8a53-22b68e50d9ed", "clientErrorCode" : "ivy:error:rest:client", "statusErrorCode" : "ivy:error:rest:client", - "resultType" : "api.rest.v6.client.BaseUriInfo", - "responseCode" : [ - "ivy.log.fatal(\"api: {0}\", result.apiAccessPoint);", - "ivy.log.fatal(\"web: {0}\", result.webAccessPoint);" - ] + "resultType" : "api.rest.v6.client.BaseUriInfo" }, "visual" : { - "at" : { "x" : 224, "y" : 64 } + "at" : { "x" : 232, "y" : 64 } }, - "connect" : { "id" : "f2", "to" : "f1" } + "connect" : { "id" : "f4", "to" : "f1" } } ] } \ No newline at end of file diff --git a/adobe-esign-connector/processes/connector/Users.p.json b/adobe-esign-connector/processes/connector/Users.p.json index e7d16e4..1d9d95e 100644 --- a/adobe-esign-connector/processes/connector/Users.p.json +++ b/adobe-esign-connector/processes/connector/Users.p.json @@ -30,13 +30,7 @@ "clientId" : "83e80d77-749b-4f53-aaf5-a796f2dab100", "clientErrorCode" : "ivy:error:rest:client", "statusErrorCode" : "ivy:error:rest:client", - "resultType" : "api.rest.v6.client.UsersInfo", - "responseCode" : [ - "import api.rest.v6.client.UsersInfoUserInfoList;", - "for(UsersInfoUserInfoList u : result.userInfoList) {", - " ivy.log.fatal(\"User: {0} {1} ({2}, isAdmin: {3})\", u.firstName, u.lastName, u.email, u.isAccountAdmin);", - "}" - ] + "resultType" : "api.rest.v6.client.UsersInfo" }, "visual" : { "at" : { "x" : 224, "y" : 64 } diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/OAuth2Feature.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/OAuth2Feature.java index 6e99b63..3ae84bc 100644 --- a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/OAuth2Feature.java +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/OAuth2Feature.java @@ -1,26 +1,33 @@ package com.axonivy.connector.adobe.esign.connector.auth; import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; import java.util.Optional; import javax.ws.rs.Priorities; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriBuilderException; + +import org.apache.commons.lang3.StringUtils; + +import com.axonivy.connector.adobe.esign.connector.auth.oauth.OAuth2BearerFilter; +import com.axonivy.connector.adobe.esign.connector.auth.oauth.OAuth2TokenRequester.AuthContext; +import com.axonivy.connector.adobe.esign.connector.auth.oauth.OAuth2UriProperty; +import com.axonivy.connector.adobe.esign.connector.enums.AdobeVariable; import ch.ivyteam.ivy.bpm.error.BpmPublicErrorBuilder; import ch.ivyteam.ivy.environment.Ivy; import ch.ivyteam.ivy.rest.client.FeatureConfig; -import ch.ivyteam.ivy.rest.client.oauth2.OAuth2BearerFilter; import ch.ivyteam.ivy.rest.client.oauth2.OAuth2RedirectErrorBuilder; -import ch.ivyteam.ivy.rest.client.oauth2.OAuth2TokenRequester.AuthContext; import ch.ivyteam.ivy.rest.client.oauth2.uri.OAuth2CallbackUriBuilder; -/** - * @since 9.2 - */ public class OAuth2Feature implements Feature { public static interface Property { @@ -30,6 +37,7 @@ public static interface Property { String AUTHORIZATION_CODE = "AUTH.code"; String SCOPE = "AUTH.scope"; String AUTH_BASE_URI = "AUTH.baseUri"; + String AUTH_INTEGRATION_KEY = "AUTH.integrationKey"; } public static final String SESSION_TOKEN = "adobe.esign.authCode"; @@ -37,13 +45,21 @@ public static interface Property { @Override public boolean configure(FeatureContext context) { var config = new FeatureConfig(context.getConfiguration(), OAuth2Feature.class); - var docuSignUri = new OAuth2UriProperty(config, Property.AUTH_BASE_URI, - "https://api.eu2.echosign.com/oauth/v2"); - var oauth2 = new OAuth2BearerFilter( - ctxt -> requestToken(ctxt, docuSignUri), - docuSignUri); - context.register(BearerTokenAuthorizationFilter.class, Priorities.AUTHORIZATION - 10); - context.register(oauth2, Priorities.AUTHORIZATION); + String intKey = config.read(Property.AUTH_INTEGRATION_KEY).orElse(""); + + // use oauth if integration key is blank, property comes as defined (${ivy.var ...) if the variable is not defined + if(StringUtils.isBlank(intKey) || intKey.startsWith("${ivy.var")) { + var adobeSignUri = new OAuth2UriProperty(config, Property.AUTH_BASE_URI, + "https://api.eu2.echosign.com/oauth/v2"); + var oauth2 = new OAuth2BearerFilter( + ctxt -> requestToken(ctxt, adobeSignUri), + adobeSignUri); + context.register(oauth2, Priorities.AUTHORIZATION); + } + else { + context.register(BearerTokenAuthorizationFilter.class, Priorities.AUTHORIZATION - 10); + } + return true; } @@ -54,26 +70,39 @@ public boolean configure(FeatureContext context) { * @param uriFactory */ private static Response requestToken(AuthContext ctxt, OAuth2UriProperty uriFactory) { - String authCode = ctxt.config.readMandatory(Property.AUTHORIZATION_CODE); + String authCode = ctxt.authCode().orElse(""); var refreshToken = ctxt.refreshToken(); if (authCode.isEmpty() && refreshToken.isEmpty()) { - authRedirectError(ctxt.config, uriFactory).throwError(); + try { + authRedirectError(ctxt.config, uriFactory).throwError(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (UriBuilderException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (URISyntaxException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } var clientId = ctxt.config.readMandatory(Property.CLIENT_ID); var clientSecret = ctxt.config.readMandatory(Property.CLIENT_SECRET); - URI tokenUri = uriFactory.getTokenUri(); - Object authRequest; + Response response = null; if (!refreshToken.isPresent()) { - authRequest = new AccessTokenRequest(authCode, clientId, clientSecret, OAuth2CallbackUriBuilder.create().toUrl().toString()); + AccessTokenRequest authRequest = new AccessTokenRequest(authCode, clientId, clientSecret, OAuth2CallbackUriBuilder.create().toUrl().toString()); + response = ctxt.target + .request() + .post(Entity.form(authRequest.paramsMap())); } else { - authRequest = new RefreshTokenRequest(refreshToken.get(), clientId, clientSecret); - tokenUri = uriFactory.getRefreshUri(); + RefreshTokenRequest authRequest = new RefreshTokenRequest(refreshToken.get(), clientId, clientSecret); + response = ctxt.target + .request() + .post(Entity.form(authRequest.paramsMap())); } - var response = ctxt.target - .request() - .post(Entity.json(authRequest)); return response; } + /** * Use the JWT grant? @@ -98,6 +127,29 @@ public AccessTokenRequest(String code, String client_id, String client_secret, S this.redirect_uri = redirect_uri; this.code = code; } + + public MultivaluedMap paramsMap() { + MultivaluedMap values = new MultivaluedHashMap<>(); + values.put("code", Arrays.asList(code)); + values.put("client_id", Arrays.asList(client_id)); + values.put("client_secret", Arrays.asList(client_secret)); + values.put("redirect_uri", Arrays.asList(redirect_uri)); + values.put("grant_type", Arrays.asList(grant_type)); + return values; + } + + @Override + public String toString() { + return String.format(""" + code=%s + client_id=%s& + client_secret=%s& + redirect_uri=%s + grant_type=%s + """, code, client_id, client_secret, redirect_uri, grant_type); + } + + } public static class RefreshTokenRequest { @@ -113,15 +165,35 @@ public RefreshTokenRequest(String refreshToken, String client_id, String client_ this.client_id = client_id; this.client_secret = client_secret; } + + public MultivaluedMap paramsMap() { + MultivaluedMap values = new MultivaluedHashMap<>(); + values.put("client_id", Arrays.asList(client_id)); + values.put("client_secret", Arrays.asList(client_secret)); + values.put("refresh_token", Arrays.asList(refresh_token)); + values.put("grant_type", Arrays.asList(grant_type)); + return values; + } + + @Override + public String toString() { + return String.format(""" + client_id=%s& + client_secret=%s& + refresh_token=%s + grant_type=%s + """, client_id, client_secret, refresh_token, grant_type); + } } - private static BpmPublicErrorBuilder authRedirectError(FeatureConfig config, OAuth2UriProperty uriFactory) { + private static BpmPublicErrorBuilder authRedirectError(FeatureConfig config, OAuth2UriProperty uriFactory) throws IllegalArgumentException, UriBuilderException, URISyntaxException { URI redirectUri = OAuth2CallbackUriBuilder.create().toUrl(); - var uri = UriBuilder.fromUri(uriFactory.getUri("auth")) + String authUri = AdobeVariable.AUTHENTICATION_URI.getValue(); + var uri = UriBuilder.fromUri(new URI(authUri)) + .queryParam("redirect_uri", redirectUri) .queryParam("response_type", "code") - .queryParam("scope", getScope(config)) .queryParam("client_id", config.readMandatory(Property.CLIENT_ID)) - .queryParam("redirect_uri", redirectUri) + .queryParam("scope", getScope(config)) .build(); Ivy.log().debug("created oauth URI: " + uri); return OAuth2RedirectErrorBuilder diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2BearerFilter.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2BearerFilter.java new file mode 100644 index 0000000..bd0a6f8 --- /dev/null +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2BearerFilter.java @@ -0,0 +1,160 @@ + +package com.axonivy.connector.adobe.esign.connector.auth.oauth; + +import java.io.IOException; +import java.util.Map; +import java.util.function.Supplier; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response.Status.Family; + +import org.apache.commons.lang3.StringUtils; + +import com.axonivy.connector.adobe.esign.connector.auth.oauth.OAuth2TokenRequester.AuthContext; +import com.axonivy.connector.adobe.esign.connector.enums.AdobeVariable; + +import ch.ivyteam.ivy.bpm.error.BpmError; +import ch.ivyteam.ivy.bpm.error.BpmPublicErrorBuilder; +import ch.ivyteam.ivy.request.IRequest; +import ch.ivyteam.ivy.rest.client.FeatureConfig; +import ch.ivyteam.ivy.rest.client.RestClientFactoryConstants; + +public class OAuth2BearerFilter implements javax.ws.rs.client.ClientRequestFilter { + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer "; + public static final String CODE_PARAM = "code"; + + private final OAuth2TokenRequester getToken; + private final OAuth2UriProperty uriFactory; + + private static final String TOKEN_SEPARATOR = ":"; + public static final String AUTH_SCOPE_PROPERTY = "AUTH.scope"; + + public static final AdobeVariable REFRESH_TOKEN_VAR = AdobeVariable.OAUTH_TOKEN; + public static final AdobeVariable ACCESS_TOKEN_VAR = AdobeVariable.ACCESS_TOKEN; + + private static final String OAUTH2_ERROR_CODE = "ivy:error:rest:client:oauth2"; + + private String property; + private Supplier name = null; + + public OAuth2BearerFilter(OAuth2TokenRequester getToken, OAuth2UriProperty uriFactory) { + this.getToken = getToken; + this.uriFactory = uriFactory; + } + + @Override + public void filter(ClientRequestContext context) throws IOException { + if (uriFactory.isAuthRequest(context.getUri())) { // already in token request: avoid stackOverflow + return; + } + + if (context.getHeaders().containsKey(AUTHORIZATION)) { // already set by other feature or explicit header + return; + } + + String accessToken = getAccessToken(context); + context.getHeaders().add(AUTHORIZATION, BEARER + accessToken); + } + + protected final String getAccessToken(ClientRequestContext context) { + FeatureConfig config = new FeatureConfig(context.getConfiguration(), getSource()); + VarTokenStore refreshTokenStore = VarTokenStore.get(REFRESH_TOKEN_VAR.getVariableName()); + var refreshToken = refreshTokenStore.getToken(); + + VarTokenStore accessTokenStore = VarTokenStore.get(ACCESS_TOKEN_VAR.getVariableName()); + var accessToken = accessTokenStore.getToken(); + + String resultToken = null; + + if(accessToken == null || accessToken.isExpired()) { + // refresh access token + if(refreshToken != null && refreshToken.hasRefreshToken()) { + accessToken = getRefreshedAccessToken(context.getClient(), config, refreshToken.refreshToken()); + accessTokenStore.setToken(accessToken); + resultToken = accessToken.accessToken(); + } + else { // get new access token + refreshToken = getNewAccessToken(context.getClient(), config); + refreshTokenStore.setToken(refreshToken); + accessTokenStore.setToken(refreshToken); + resultToken = refreshToken.accessToken(); + } + } + else { // use existing token + resultToken = accessToken.accessToken(); + } + + if (accessToken != null && !accessToken.hasAccessToken()) { + accessTokenStore.setToken(null); + authError().withMessage("Failed to read 'access_token' from " + refreshToken).throwError(); + } + + return resultToken; + } + + String createKey(FeatureConfig config) { + Object clientId = config.readMandatory(RestClientFactoryConstants.PROPERTY_CLIENT_ID); + var key = new StringBuilder((String) clientId); + if (property != null) { + key.append(TOKEN_SEPARATOR).append(config.readMandatory(property)); + } else { + config.read(AUTH_SCOPE_PROPERTY).ifPresent(scope -> key.append(TOKEN_SEPARATOR).append(scope)); + } + if (name != null) { + key.append(TOKEN_SEPARATOR).append(name.get()); + } + return key.toString(); + } + + private Class getSource() { + Class type = getToken.getClass(); + Class declaring = type.getDeclaringClass(); + if (declaring != null) { + return declaring; + } + return type; + } + + private Token getNewAccessToken(Client client, FeatureConfig config) { + return getAccessToken(client, config, null); + } + + private Token getRefreshedAccessToken(Client client, FeatureConfig config, String refreshToken) { + return getAccessToken(client, config, refreshToken); + } + + private Token getAccessToken(Client client, FeatureConfig config, String refreshToken) { + GenericType> map = new GenericType<>(Map.class); + + // use refresh uri for refresh token + var tokenUri = StringUtils.isNotBlank(refreshToken) ? uriFactory.getRefreshUri() : uriFactory.getTokenUri(); + String authCode = getAuthCode(); + var authContext = new AuthContext(client.target(tokenUri), config, authCode, refreshToken); + var response = getToken.requestToken(authContext); + if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) { + return new Token(response.readEntity(map)); + } + throw authError().withMessage("Failed to get or refresh access token: " + response) + .withAttribute("status", response.getStatus()) + .withAttribute("payload", response.readEntity(String.class)).build(); + } + + protected String getAuthCode() { + var request = IRequest.current(); + if (request == null) { + return null; + } + var authCode = request.getFirstParameter(CODE_PARAM); + if (StringUtils.isBlank(authCode)) { + return null; + } + return authCode; + } + + private static BpmPublicErrorBuilder authError() { + return BpmError.create(OAUTH2_ERROR_CODE); + } +} diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2TokenRequester.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2TokenRequester.java new file mode 100644 index 0000000..8030b7f --- /dev/null +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2TokenRequester.java @@ -0,0 +1,57 @@ +package com.axonivy.connector.adobe.esign.connector.auth.oauth; + +import java.util.Optional; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import ch.ivyteam.ivy.rest.client.FeatureConfig; +import ch.ivyteam.ivy.rest.client.oauth2.uri.OAuth2UriProvider; + +/** + * Requests a OAuth2 bearer access token + */ +public interface OAuth2TokenRequester { + /** + * Requests a OAuth2 bearer access token + * @param ctxt authentication context + * @return response from the remote accessToken request. Yet with an unread entity. + */ + Response requestToken(AuthContext ctxt); + + /** + * The authentication context to be used to fire an 'accessToken' request. + */ + public static class AuthContext { + /** JAX-RS client pre-configured to call the accessToken URI defined by {@link OAuth2UriProvider#getTokenUri()} */ + public final WebTarget target; + + /** Current feature configuration provided by Rest Client properties or programmatic features. */ + public final FeatureConfig config; + + private final Optional authCode; + private final Optional refreshToken; + + AuthContext(WebTarget target, FeatureConfig config, String authCode, String refreshToken) { + this.target = target; + this.config = config; + this.authCode = Optional.ofNullable(authCode); + this.refreshToken = Optional.ofNullable(refreshToken); + } + + /** + * @return User access code query parameter obtained from an OAuth2 '/auth' user consent request. Or {@link Optional#empty()} + * when an implicit grant is being implemented. + */ + public Optional authCode() { + return authCode; + } + + /** + * @return Refresh token if one is available + */ + public Optional refreshToken() { + return refreshToken; + } + } +} \ No newline at end of file diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/OAuth2UriProperty.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2UriProperty.java similarity index 60% rename from adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/OAuth2UriProperty.java rename to adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2UriProperty.java index 4d020bc..69942a9 100644 --- a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/OAuth2UriProperty.java +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/OAuth2UriProperty.java @@ -1,10 +1,14 @@ -package com.axonivy.connector.adobe.esign.connector.auth; +package com.axonivy.connector.adobe.esign.connector.auth.oauth; import java.net.URI; import ch.ivyteam.ivy.rest.client.FeatureConfig; public class OAuth2UriProperty extends ch.ivyteam.ivy.rest.client.oauth2.uri.OAuth2UriProperty { + + public static final String TOKEN_RELATIVE_PATH = "token"; + public static final String REFRESH_RELATIVE_PATH = "refresh"; + public OAuth2UriProperty(FeatureConfig config, String defaultBaseUri) { super(config, defaultBaseUri); @@ -15,7 +19,11 @@ public OAuth2UriProperty(FeatureConfig config, String baseUriPropertyName, Strin } public URI getRefreshUri() { - return getUri("refresh"); + return getUri(REFRESH_RELATIVE_PATH); } + @Override + public URI getTokenUri() { + return getUri(TOKEN_RELATIVE_PATH); + } } diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/Token.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/Token.java new file mode 100644 index 0000000..de6fa7c --- /dev/null +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/Token.java @@ -0,0 +1,57 @@ +package com.axonivy.connector.adobe.esign.connector.auth.oauth; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import ch.ivyteam.util.date.Now; + +public class Token { + private final Instant created; + private final Map values; + + public Token(Map values) { + this.values = values; + this.created = Now.asInstant(); + } + + public Object value(String name) { + return values.get(name); + } + + public boolean hasAccessToken() { + return StringUtils.isNotBlank(accessToken()); + } + + public String accessToken() { + return (String)values.get("access_token"); + } + + public boolean hasRefreshToken() { + return StringUtils.isNotBlank(refreshToken()); + } + + public String refreshToken() { + return (String)values.get("refresh_token"); + } + + public boolean isExpired() { + var expiresAt = created.plus(expiresIn(), ChronoUnit.SECONDS); + return Instant.now().isAfter(expiresAt); + } + + private int expiresIn() { + var expiresIn = (Double)values.get("expires_in"); + if (expiresIn == null) { + return Integer.MAX_VALUE; + } + return expiresIn.intValue(); + } + + @Override + public String toString() { + return "Token [created=" + created + " expired=" + isExpired() + " values=" + values + "]"; + } +} \ No newline at end of file diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/VarTokenStore.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/VarTokenStore.java new file mode 100644 index 0000000..f327bb5 --- /dev/null +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/auth/oauth/VarTokenStore.java @@ -0,0 +1,49 @@ +package com.axonivy.connector.adobe.esign.connector.auth.oauth; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +import ch.ivyteam.ivy.environment.Ivy; + +/** + * Class for storing OAuth token to Variable + * @author stefan.masek + * + */ +public class VarTokenStore { + private final String varName; + + public static VarTokenStore get(String varName) { + return new VarTokenStore(varName); + } + + VarTokenStore(String varName) { + this.varName = varName; + } + + /** + * Loads Variable and tries to create the token object {@link Token} + * @return + */ + public Token getToken() { + String tokenVar = Ivy.var().get(varName); + Gson gson = new Gson(); + Token token = null; + try { + token = gson.fromJson(tokenVar, Token.class); + } catch (JsonSyntaxException e) { + Ivy.log().warn("Couldn't create Token object from variable", e); + } + return token; + } + + /** + * Stores token object {@link Token} as a json to Variable + * @param token + */ + public void setToken(Token token) { + Gson gson = new Gson(); + String tokenString = gson.toJson(token); + Ivy.var().set(varName, tokenString); + } +} diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/enums/AdobeVariable.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/enums/AdobeVariable.java index f152a39..7aed7a2 100644 --- a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/enums/AdobeVariable.java +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/enums/AdobeVariable.java @@ -1,14 +1,25 @@ package com.axonivy.connector.adobe.esign.connector.enums; +import ch.ivyteam.ivy.environment.Ivy; + public enum AdobeVariable { - BASE_URI("adobe-sign-connector.baseUri"), HOST("adobe-sign-connector.host"), - INTEGRATION_KEY("adobe-sign-connector.integrationKey"), RETURN_PAGE("adobe-sign-connector.returnPage"), - PERMISSIONS("adobe-sign-connector.permissions"), CLIENT_ID("adobe-sign-connector.clientId"), - CLIENT_SECRET("adobe-sign-connector.clientSecret"), APP_ID("adobe-sign-connector.appId"), - SECRET_KEY("adobe-sign-connector.secretKey"), USE_APP_PERMISSIONS("adobe-sign-connector.useAppPermissions"), - CODE("adobe-sign-connector.code"), USE_USER_PASS_FLOW_ENABLED("adobe-sign-connector.useUserPassFlow.enabled"), + BASE_URI("adobe-sign-connector.baseUri"), + HOST("adobe-sign-connector.host"), + INTEGRATION_KEY("adobe-sign-connector.integrationKey"), + RETURN_PAGE("adobe-sign-connector.returnPage"), + PERMISSIONS("adobe-sign-connector.permissions"), + CLIENT_ID("adobe-sign-connector.clientId"), + CLIENT_SECRET("adobe-sign-connector.clientSecret"), + APP_ID("adobe-sign-connector.appId"), + SECRET_KEY("adobe-sign-connector.secretKey"), + USE_APP_PERMISSIONS("adobe-sign-connector.useAppPermissions"), + CODE("adobe-sign-connector.code"), + USE_USER_PASS_FLOW_ENABLED("adobe-sign-connector.useUserPassFlow.enabled"), USE_USER_PASS_FLOW_USER("adobe-sign-connector.useUserPassFlow.user"), - USE_USER_PASS_FLOW_PASS("adobe-sign-connector.useUserPassFlow.pass"); + USE_USER_PASS_FLOW_PASS("adobe-sign-connector.useUserPassFlow.pass"), + OAUTH_TOKEN("adobe-sign-connector.oauthToken"), + ACCESS_TOKEN("adobe-sign-connector.accessToken"), + AUTHENTICATION_URI("adobe-sign-connector.authenticationUri"); private String variableName; @@ -19,4 +30,17 @@ private AdobeVariable(String variableName) { public String getVariableName() { return variableName; } + + public String getValue() { + return Ivy.var().get(variableName); + } + + /** + * Sets new value to the Variable + * @param newValue + * @return old value that was set before. Empty string if it was not defined. + */ + public String updateValue(String newValue) { + return Ivy.var().set(variableName, newValue); + } } diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/rest/WebHookService.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/rest/WebHookService.java index 8eb11be..b4c5e2c 100644 --- a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/rest/WebHookService.java +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/rest/WebHookService.java @@ -1,7 +1,5 @@ package com.axonivy.connector.adobe.esign.connector.rest; -import java.util.Optional; - import javax.annotation.security.PermitAll; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -33,7 +31,6 @@ public class WebHookService { private static final String CLIENT_ID_HEADER = "X-ADOBESIGN-CLIENTID"; - private static final String CLIENT_ID_VAR = "ivy.var.adobe-sign-connector.clientId"; @GET @Produces("application/json") @@ -55,10 +52,6 @@ public void notification(JsonNode node) { Ivy.log().info(node); } - private boolean verifyClientId(String clientId) { - return Optional.ofNullable(Ivy.var().get(CLIENT_ID_VAR)).map(s -> s.compareTo(clientId) == 0).orElse(false); - } - private record ClientIdResponse(String xAdobeSignClientId) {}; } diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/AdminSetupService.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/AdminSetupService.java new file mode 100644 index 0000000..fddf322 --- /dev/null +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/AdminSetupService.java @@ -0,0 +1,136 @@ +package com.axonivy.connector.adobe.esign.connector.service; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status.Family; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriBuilderException; + +import org.primefaces.PrimeFaces; + +import com.axonivy.connector.adobe.esign.connector.AdminSetup.AdminSetupData; +import com.axonivy.connector.adobe.esign.connector.auth.OAuth2Feature.AccessTokenRequest; +import com.axonivy.connector.adobe.esign.connector.auth.oauth.OAuth2UriProperty; +import com.axonivy.connector.adobe.esign.connector.auth.oauth.Token; +import com.axonivy.connector.adobe.esign.connector.auth.oauth.VarTokenStore; +import com.axonivy.connector.adobe.esign.connector.enums.AdobeVariable; + +import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.ivy.request.EngineUriResolver; + +public class AdminSetupService { + private static final String OAUTH_RESUME_START_FRIENDLY_PATH = "Setup/AdminSetup/oauthResume.ivp"; + + /** + * Initializes variables for the dialog data + * @param data dialog data + */ + public static void initVars(AdminSetupData data) { + data.setAuthenticationUri(AdobeVariable.AUTHENTICATION_URI.getValue()); + data.setBaseUri(AdobeVariable.BASE_URI.getValue()); + data.setClientId(AdobeVariable.CLIENT_ID.getValue()); + data.setClientSecret(AdobeVariable.CLIENT_SECRET.getValue()); + data.setHost(AdobeVariable.HOST.getValue()); + data.setIntegrationKey(AdobeVariable.INTEGRATION_KEY.getValue()); + data.setOauthToken(AdobeVariable.OAUTH_TOKEN.getValue()); + data.setPermissions(AdobeVariable.PERMISSIONS.getValue()); + data.setReturnPage(AdobeVariable.RETURN_PAGE.getValue()); + data.setAccessToken(AdobeVariable.ACCESS_TOKEN.getValue()); + } + + /** + * Updates Variables from dialog data + * @param data dialog data + */ + public static void updateVars(AdminSetupData data) { + AdobeVariable.AUTHENTICATION_URI.updateValue(data.getAuthenticationUri()); + AdobeVariable.BASE_URI.updateValue(data.getBaseUri()); + AdobeVariable.CLIENT_ID.updateValue(data.getClientId()); + AdobeVariable.CLIENT_SECRET.updateValue(data.getClientSecret()); + AdobeVariable.HOST.updateValue(data.getHost()); + AdobeVariable.INTEGRATION_KEY.updateValue(data.getIntegrationKey()); + AdobeVariable.PERMISSIONS.updateValue(data.getPermissions()); + AdobeVariable.RETURN_PAGE.updateValue(data.getReturnPage()); + // dont update oauth token variables + } + + /** + * Opens Adobe OAuth authentication page with parameters from Variables + * @throws IOException + * @throws IllegalArgumentException + * @throws UriBuilderException + * @throws URISyntaxException + */ + public static void authRedirect() + throws IOException, IllegalArgumentException, UriBuilderException, URISyntaxException { + var clientId = AdobeVariable.CLIENT_ID.getValue(); + var scope = AdobeVariable.PERMISSIONS.getValue(); + String redirectUri = createRedirectUrl(); + + String authUri = AdobeVariable.AUTHENTICATION_URI.getValue(); + URI uri = UriBuilder.fromUri(new URI(authUri)) + .queryParam("redirect_uri", redirectUri) + .queryParam("response_type", "code") + .queryParam("client_id", clientId) + .queryParam("scope", scope) + .build(); + Ivy.log().debug("created oauth URI: " + uri); + PrimeFaces.current().executeScript("window.open('" + uri.toString() + "', '_top')"); + } + + /** + * Tries to get the OAuth access token with given code and parameters loaded. + * Stores the token in the Variable {@link AdobeVariable#OAUTH_TOKEN} + * + * @param authCode + * @throws URISyntaxException + */ + public static void getNewAccessToken(String authCode) throws URISyntaxException { + var clientId = AdobeVariable.CLIENT_ID.getValue(); + var clientSecret = AdobeVariable.CLIENT_SECRET.getValue(); + UriBuilder tokenBaseUri = UriBuilder.fromUri(new URI(AdobeVariable.BASE_URI.getValue())); + + // get token stores for refresh and access tokens + VarTokenStore refreshTokenStore = VarTokenStore.get(AdobeVariable.OAUTH_TOKEN.getVariableName()); + VarTokenStore accessTokenStore = VarTokenStore.get(AdobeVariable.ACCESS_TOKEN.getVariableName()); + + //request new token + Client client = ClientBuilder.newClient(); + tokenBaseUri.path(OAuth2UriProperty.TOKEN_RELATIVE_PATH); + AccessTokenRequest authRequest = new AccessTokenRequest(authCode, clientId, clientSecret, + createRedirectUrl()); + WebTarget target = client.target(tokenBaseUri); + Response response = target.request().post(Entity.form(authRequest.paramsMap())); + + if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) { + GenericType> map = new GenericType<>(Map.class); + // store new token + Token newToken = new Token(response.readEntity(map)); + refreshTokenStore.setToken(newToken); + accessTokenStore.setToken(newToken); + } else { + Ivy.log().error("failed to get access token: " + response); + } + } + + /** + * Creates URI for redirecting after Adobe authentication + * + * @return + */ + public static String createRedirectUrl() { + String startUri = ProcessStartService + .findRelativeUrlByProcessStartFriendlyRequestPath(OAUTH_RESUME_START_FRIENDLY_PATH); + URI redirectUri = UriBuilder.fromUri(EngineUriResolver.instance().external()).path(startUri).build(); + return redirectUri.toString(); + } +} diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/AdobeSignService.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/AdobeSignService.java index 7876082..0076d60 100644 --- a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/AdobeSignService.java +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/AdobeSignService.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Objects; +import javax.servlet.http.HttpServletRequest; + import com.axonivy.connector.adobe.esign.connector.rest.DownloadResult; import com.axonivy.connector.adobe.esign.connector.rest.UploadWrapper; @@ -36,6 +38,7 @@ import ch.ivyteam.ivy.environment.Ivy; import ch.ivyteam.ivy.process.call.SubProcessCall; import ch.ivyteam.ivy.process.call.SubProcessCallResult; +import ch.ivyteam.ivy.request.IHttpRequest; /** * Service class to call Adobe Sign services and create necessary objects @@ -143,7 +146,9 @@ public AgreementCreationInfo buildSimpleAgreementWithFormFields(String name, Lis agreement.setEmailOption(createAllDisabledSendOptions()); - agreement.postSignOption(new AgreementsPostSignOption().redirectUrl(Ivy.var().get("adobe-sign-connector.returnPage"))); + String baseUrl = getRequestBaseUrl(); + String fullUrl = baseUrl + Ivy.var().get("adobe-sign-connector.returnPage"); + agreement.postSignOption(new AgreementsPostSignOption().redirectUrl(fullUrl)); if(Objects.nonNull(formFieldGenerators)) { agreement.setFormFieldGenerators(formFieldGenerators); @@ -192,7 +197,9 @@ public AgreementCreationInfo buildSimpleAgreementFor2ParticipantGroups(String na agreement.setEmailOption(createAllDisabledSendOptions()); - agreement.postSignOption(new AgreementsPostSignOption().redirectUrl(Ivy.var().get("adobe-sign-connector.returnPage"))); + String baseUrl = getRequestBaseUrl(); + String fullUrl = baseUrl + Ivy.var().get("adobe-sign-connector.returnPage"); + agreement.postSignOption(new AgreementsPostSignOption().redirectUrl(fullUrl)); return agreement; } @@ -433,4 +440,21 @@ private void handleError(SubProcessCallResult callResult) { } } + /** + * Extracts the base url from actual request. + * Example: http://localhost:8081 + * @return + */ + private String getRequestBaseUrl() { + IHttpRequest req = (IHttpRequest) Ivy.request(); + HttpServletRequest request = req.getHttpServletRequest(); + StringBuilder baseUrlBuilder = new StringBuilder(); + baseUrlBuilder.append(request.getScheme()).append("://").append(request.getServerName()); + int port = request.getServerPort(); + if (port != 80 && port != 443) { + baseUrlBuilder.append(":").append(port); + } + return baseUrlBuilder.toString(); + } + } diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/ProcessStartService.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/ProcessStartService.java new file mode 100644 index 0000000..54fdafa --- /dev/null +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/service/ProcessStartService.java @@ -0,0 +1,76 @@ +package com.axonivy.connector.adobe.esign.connector.service; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; + +import ch.ivyteam.ivy.application.ActivityState; +import ch.ivyteam.ivy.application.IApplication; +import ch.ivyteam.ivy.application.IProcessModel; +import ch.ivyteam.ivy.application.IProcessModelVersion; +import ch.ivyteam.ivy.application.app.IApplicationRepository; +import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.ivy.security.ISecurityContext; +import ch.ivyteam.ivy.security.exec.Sudo; +import ch.ivyteam.ivy.workflow.IProcessStart; +import ch.ivyteam.ivy.workflow.IWorkflowProcessModelVersion; + +/** + * Service for working with process starts. Code is from portal's + * ProcessStartAPI + */ +public class ProcessStartService { + /** + * Find start link from friendly request path + * + * @param friendlyRequestPath friendly path e.g "Start + * Processes/UserExampleGuide/userExampleGuide.ivp" + * @return start link or empty string + */ + public static String findRelativeUrlByProcessStartFriendlyRequestPath(String friendlyRequestPath) { + IProcessStart processStart = findProcessStartByUserFriendlyRequestPath(friendlyRequestPath); + return processStart != null ? processStart.getLink().getRelative() : StringUtils.EMPTY; + } + + private static IProcessStart findProcessStartByUserFriendlyRequestPath(String requestPath) { + return Sudo.get(() -> { + IProcessStart processStart = getProcessStart(requestPath, Ivy.request().getProcessModelVersion()); + if (processStart != null) { + return processStart; + } + List applicationsInSecurityContext = IApplicationRepository.instance() + .allOf(ISecurityContext.current()); + for (IApplication app : applicationsInSecurityContext) { + IProcessStart findProcessStart = filterPMV(requestPath, app).findFirst().orElse(null); + if (findProcessStart != null) { + return findProcessStart; + } + } + return null; + }); + } + + private static IProcessStart getProcessStart(String requestPath, IProcessModelVersion processModelVersion) { + return IWorkflowProcessModelVersion.of(processModelVersion) + .findStartElementByUserFriendlyRequestPath(requestPath); + } + + private static Stream filterPMV(String requestPath, IApplication application) { + return filterActivePMVOfApp(application).map(p -> getProcessStart(requestPath, p)).filter(Objects::nonNull); + } + + private static Stream filterActivePMVOfApp(IApplication application) { + return application.getProcessModelsSortedByName().stream().filter(pm -> isActive(pm)) + .map(IProcessModel::getReleasedProcessModelVersion).filter(pmv -> isActive(pmv)); + } + + private static boolean isActive(IProcessModelVersion processModelVersion) { + return processModelVersion != null && processModelVersion.getActivityState() == ActivityState.ACTIVE; + } + + private static boolean isActive(IProcessModel processModel) { + return processModel.getActivityState() == ActivityState.ACTIVE; + } +} \ No newline at end of file diff --git a/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/ui/bean/AdminSetupBean.java b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/ui/bean/AdminSetupBean.java new file mode 100644 index 0000000..8f20914 --- /dev/null +++ b/adobe-esign-connector/src/com/axonivy/connector/adobe/esign/connector/ui/bean/AdminSetupBean.java @@ -0,0 +1,19 @@ +package com.axonivy.connector.adobe.esign.connector.ui.bean; + +import javax.faces.bean.ManagedBean; +import javax.faces.bean.ViewScoped; + +import com.axonivy.connector.adobe.esign.connector.enums.AdobeVariable; +import com.axonivy.connector.adobe.esign.connector.service.AdminSetupService; + +@ManagedBean +@ViewScoped +public class AdminSetupBean { + public String getVariableName(AdobeVariable var) { + return var.getVariableName(); + } + + public String getRedirectUri() { + return AdminSetupService.createRedirectUrl(); + } +} diff --git a/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetup.rddescriptor b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetup.rddescriptor new file mode 100644 index 0000000..ae605f0 --- /dev/null +++ b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetup.rddescriptor @@ -0,0 +1,7 @@ + + + + viewTechnology + JSF + + diff --git a/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetup.xhtml b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetup.xhtml new file mode 100644 index 0000000..ca0f296 --- /dev/null +++ b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetup.xhtml @@ -0,0 +1,162 @@ + + + + AdminSetup + + +

+ #{ivy.cms.co('/Dialogs/com/axonivy/connector/adobe/esign/connector/AdminSetup/Title')} +

+ + + +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetupData.ivyClass b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetupData.ivyClass new file mode 100644 index 0000000..61bd728 --- /dev/null +++ b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetupData.ivyClass @@ -0,0 +1,22 @@ +AdminSetupData #class +com.axonivy.connector.adobe.esign.connector.AdminSetup #namespace +host String #field +host PERSISTENT #fieldModifier +baseUri String #field +baseUri PERSISTENT #fieldModifier +authenticationUri String #field +authenticationUri PERSISTENT #fieldModifier +clientId String #field +clientId PERSISTENT #fieldModifier +clientSecret String #field +clientSecret PERSISTENT #fieldModifier +permissions String #field +permissions PERSISTENT #fieldModifier +integrationKey String #field +integrationKey PERSISTENT #fieldModifier +returnPage String #field +returnPage PERSISTENT #fieldModifier +oauthToken String #field +oauthToken PERSISTENT #fieldModifier +accessToken String #field +accessToken PERSISTENT #fieldModifier diff --git a/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetupProcess.p.json b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetupProcess.p.json new file mode 100644 index 0000000..4a06258 --- /dev/null +++ b/adobe-esign-connector/src_hd/com/axonivy/connector/adobe/esign/connector/AdminSetup/AdminSetupProcess.p.json @@ -0,0 +1,136 @@ +{ + "format" : "10.0.0", + "id" : "18A8364B91F33AB9", + "kind" : "HTML_DIALOG", + "config" : { + "data" : "com.axonivy.connector.adobe.esign.connector.AdminSetup.AdminSetupData" + }, + "elements" : [ { + "id" : "f0", + "type" : "HtmlDialogStart", + "name" : "start()", + "config" : { + "callSignature" : "start", + "guid" : "18A8364B921D66CB" + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + }, { + "id" : "f1", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 224, "y" : 64 } + } + }, { + "id" : "f3", + "type" : "HtmlDialogEventStart", + "name" : "save", + "config" : { + "guid" : "18A8364B924568D6" + }, + "visual" : { + "at" : { "x" : 88, "y" : 280 } + }, + "connect" : { "id" : "f5", "to" : "f4" } + }, { + "id" : "f6", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 352, "y" : 280 } + } + }, { + "id" : "f4", + "type" : "Script", + "name" : "update vars", + "config" : { + "security" : "system", + "output" : { + "code" : [ + "import com.axonivy.connector.adobe.esign.connector.service.AdminSetupService;", + "AdminSetupService.updateVars(in);" + ] + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 280 } + }, + "connect" : { "id" : "f7", "to" : "f6" } + }, { + "id" : "f8", + "type" : "HtmlDialogMethodStart", + "name" : "preRender(ComponentSystemEvent)", + "config" : { + "callSignature" : "preRender", + "guid" : "18A8A15BDBC42375", + "input" : { + "params" : [ + { "name" : "event", "type" : "javax.faces.event.ComponentSystemEvent" } + ] + } + }, + "visual" : { + "at" : { "x" : 96, "y" : 152 }, + "labelOffset" : { "x" : 19, "y" : 51 } + }, + "connect" : { "id" : "f12", "to" : "f11" } + }, { + "id" : "f9", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 352, "y" : 152 } + } + }, { + "id" : "f11", + "type" : "Script", + "name" : "init vars", + "config" : { + "security" : "system", + "output" : { + "code" : [ + "import com.axonivy.connector.adobe.esign.connector.service.AdminSetupService;", + "AdminSetupService.initVars(in);" + ] + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 152 } + }, + "connect" : { "id" : "f10", "to" : "f9" } + }, { + "id" : "f13", + "type" : "HtmlDialogEventStart", + "name" : "requestToken", + "config" : { + "guid" : "18A8D57408743430" + }, + "visual" : { + "at" : { "x" : 88, "y" : 392 } + }, + "connect" : { "id" : "f20", "to" : "f18" } + }, { + "id" : "f14", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 352, "y" : 392 } + } + }, { + "id" : "f18", + "type" : "Script", + "name" : "OAuth redirect", + "config" : { + "security" : "system", + "output" : { + "code" : [ + "import com.axonivy.connector.adobe.esign.connector.service.AdminSetupService;", + "AdminSetupService.authRedirect();" + ] + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 392 } + }, + "connect" : { "id" : "f17", "to" : "f14" } + } ] +} \ No newline at end of file diff --git a/adobe-esign-connector/webContent/layouts/frame-10-full-width.xhtml b/adobe-esign-connector/webContent/layouts/frame-10-full-width.xhtml new file mode 100644 index 0000000..768d89e --- /dev/null +++ b/adobe-esign-connector/webContent/layouts/frame-10-full-width.xhtml @@ -0,0 +1,60 @@ + + + + + + + + + + <ui:insert name="title">Ivy Html Dialog</ui:insert> + + + + + + + + + +
+ + default content + +
+ + + + + + + +
+ \ No newline at end of file diff --git a/adobe-esign-connector/webContent/layouts/includes/exception-details.xhtml b/adobe-esign-connector/webContent/layouts/includes/exception-details.xhtml new file mode 100644 index 0000000..a4979dc --- /dev/null +++ b/adobe-esign-connector/webContent/layouts/includes/exception-details.xhtml @@ -0,0 +1,109 @@ + + + + + + +

+ +

+ + +

Error id

+

#{errorPage.exceptionId}

+

Error Timestamp

+

#{errorPage.createdAt}

+
+ + + + +

Attributes

+
+ + + + + + + + + + + + + + + +
NameValue
+
+
+

Thrown by

+

Process: + +
Element: + +

+
+ + +

Process call stack

+ +
#{caller.callerElement}
+
+
+ +

Technical cause

+
#{causedBy.class.simpleName}: #{causedBy.message.trim()}
+
+
+ +

Request Uri

+

#{errorPage.getRequestUri()}

+
+

Servlet

+

#{errorPage.getServletName()}

+
+ +

Application

+

#{errorPage.applicationName}

+
+ + +

Thread local values

+
+ + + + + + + + + + + + + + + +
KeyValue
+
+
+
+ +

Stack-Trace

+
#{errorPage.getStackTrace()}
+
+ diff --git a/adobe-esign-connector/webContent/layouts/includes/exception.xhtml b/adobe-esign-connector/webContent/layouts/includes/exception.xhtml new file mode 100644 index 0000000..2303e7c --- /dev/null +++ b/adobe-esign-connector/webContent/layouts/includes/exception.xhtml @@ -0,0 +1,47 @@ + + + + + + + + + +
+
+ + +
+ + + + + + + + + +
+ + \ No newline at end of file diff --git a/adobe-esign-connector/webContent/layouts/includes/footer.xhtml b/adobe-esign-connector/webContent/layouts/includes/footer.xhtml new file mode 100644 index 0000000..3eb052b --- /dev/null +++ b/adobe-esign-connector/webContent/layouts/includes/footer.xhtml @@ -0,0 +1,18 @@ + + + +
+ + #{ivyAdvisor.applicationName} + + +
+
+ + \ No newline at end of file diff --git a/adobe-esign-connector/webContent/layouts/includes/progress-loader.xhtml b/adobe-esign-connector/webContent/layouts/includes/progress-loader.xhtml new file mode 100644 index 0000000..0d68a75 --- /dev/null +++ b/adobe-esign-connector/webContent/layouts/includes/progress-loader.xhtml @@ -0,0 +1,15 @@ + + + + +
+
+
Loading...
+
+
+ + + +
+
\ No newline at end of file diff --git a/pom.xml b/pom.xml index 517336b..f8b35a4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,9 @@ 4.0.0 - com.axonivy.market - my-product - my-product-modules - 10.0.0-SNAPSHOT + com.axonivy.connector.adobe.esign + adobe-esign-connector + adobe-esign-connector-modules + 10.0.11-SNAPSHOT pom