What's new
This page lists all of the changes added in the 2026.03.x release.
2026.03.2 release
This section lists all of the changes added in patch 2 of the 2026.03 release.
New Jutro version
InsuranceSuite application affected: N/A
Digital reference application affected: All Digital reference applications
Digital reference applications now use Jutro patch version 10.13.3. This
brings enhanced security to the Jutro Design System. For more information, see What's new in Jutro patch 10.13.3.
Response header configuration changes
InsuranceSuite application affected: PolicyCenter, ClaimCenter
Digital reference application affected: CustomerEngage Account Management, CustomerEngage Account Management for ClaimCenter, ProducerEngage, ProducerEngage for ClaimCenter, ServiceRepEngage
- gsrc/edge/capabilities/gateway/document/DocumentUploadHandler.gs
- gsrc/edge/capabilities/policy/document/PolicyDocumentUploadHandler.gs
- gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs
- gsrc/edge/security/fileupload/FileUploadUtil.gs
- gsrc/edge/capabilities/claim/document/ClaimDocumentUploadHandler.gs
Changes to the document upload handler
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in DocumentUploadHandler.gs:
package edge.capabilities.gateway.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.security.fileupload.IFileUploadPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
class DocumentUploadHandler extends AbstractRpcHandler {
var _documentPlugin : IDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("fileUploadPlugin", "Plugin used to verify file upload validity")
construct(aDocumentPlugin : IDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
fileUploadPlugin : IFileUploadPlugin){
this._documentPlugin = aDocumentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Uploads a document
*
* <dl>
* <dt>Calls:</dt>
* <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
* <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
* <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
* <dt>Throws:</dt>
* <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
* <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
* <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
* </dl>
* @param documentDTO
* @param fileItem
* @return document metadata response
*/
@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
"at [0] is the document metadata; at [1] is the item to be uploaded")
@ApidocAvailableSince("5.0")
function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {
try {
// validate upload token is still valid
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException){
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
/** Some browsers (or fileupload library on some browsers) do not populate this field). */
if (documentDto.MimeType == null) {
documentDto.MimeType = documentFile.ContentType
}
final var doc = Bundle.resolveInTransaction( \ bundle -> {
try {
final var _doc = _documentPlugin.createDocument(documentDto)
// add the document to the dms
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, _doc)
return _doc
} finally {
documentFile.InputStream.close()
}
})
return _documentPlugin.toDTO(doc)
}
}package edge.capabilities.gateway.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.security.fileupload.DefaultFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil
class DocumentUploadHandler extends AbstractRpcHandler {
private static final var LOG = Logger.forCategory(DocumentUploadHandler.Type.QName)
private static final var TIKA = new Tika()
var _documentPlugin : IDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
@InjectableNode
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
construct(aDocumentPlugin : IDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin){
this._documentPlugin = aDocumentPlugin
this._documentSessionPlugin = documentSessionPlugin
}
/**
* Uploads a document
*
* <dl>
* <dt>Calls:</dt>
* <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
* <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
* <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
* <dt>Throws:</dt>
* <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
* <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
* <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
* </dl>
* @param documentDTO
* @param fileItem
* @return document metadata response
*/
@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
"at [0] is the document metadata; at [1] is the item to be uploaded")
@ApidocAvailableSince("5.0")
function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {
try {
// validate upload token is still valid
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException){
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if (documentDto.Name != documentFile.Name) {
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
if (documentDto.MimeType == null) {
documentDto.MimeType = documentFile.ContentType
}
FileUploadUtil.enforceMaxUploadSize(documentFile)
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
// Pass filename so we can disambiguate OOXML when Tika reports application/zip.
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
if (!isAllowedDetectedType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
}
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
documentDto.MimeType = detectedType
final var doc = Bundle.resolveInTransaction(\ bundle -> {
final var _doc = _documentPlugin.createDocument(documentDto)
var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
try {
Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, _doc)
} finally {
uploadInputStream.close()
}
return _doc
})
return _documentPlugin.toDTO(doc)
}
private static function isAllowedDetectedType(detectedType : String) : boolean {
return new DefaultFileUploadPlugin().canUploadContentType(detectedType)
}
}Changes to the policy document upload handler
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in PolicyDocumentUploadHandler.gs:
package edge.capabilities.policy.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
private var _policyAuthCheck : IPolicyAccessPlugin
private var _documentPlugin : IPolicyDocumentsPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("policyAuthCheck", "Policy access policy")
@Param("documentPlugin", "Plugin used to access documents associated with the policy")
@Param("documentSessionPlugin", "Document session management plugin")
construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
documentSessionPlugin : IDocumentSessionPlugin, fileUploadPlugin : IFileUploadPlugin) {
this._policyAuthCheck = policyAuthCheck
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._fileUploadPlugin = fileUploadPlugin
}
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. " +
"NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
@ApidocAvailableSince("5.0")
function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
try {
try {
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)
if (policyPeriod == null) {
throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
}
if (!_policyAuthCheck.hasAccess(policyPeriod)) {
throw new NoAuthorityException()
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
policyPeriod = bundle.add(policyPeriod)
final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
return doc
})
/* Should do this outside the transactions as public ID is not set inside the bundle. */
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}package edge.capabilities.policy.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses edge.security.fileupload.FileUploadUtil
uses edge.security.fileupload.DefaultFileUploadPlugin
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
private var _policyAuthCheck : IPolicyAccessPlugin
private var _documentPlugin : IPolicyDocumentsPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private static final var TIKA = new Tika()
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
@InjectableNode
@Param("policyAuthCheck", "Policy access policy")
@Param("documentPlugin", "Plugin used to access documents associated with the policy")
@Param("documentSessionPlugin", "Document session management plugin")
construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
documentSessionPlugin : IDocumentSessionPlugin) {
this._policyAuthCheck = policyAuthCheck
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
}
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. " +
"NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
@ApidocAvailableSince("5.0")
function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
try {
try {
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name != documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
// Enforce max upload size
FileUploadUtil.enforceMaxUploadSize(documentFile)
// Read file bytes with limit
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
// Detect and normalize MIME type
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
// Enforce PDF name/content consistency
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
// Cross-check filename extension against bytes-detected MIME
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
// Centralized allowed content type check
if (!new DefaultFileUploadPlugin().canUploadContentType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
}
// Validate PDF/image content
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
// Persist the validated MIME on the DTO so stored metadata matches
documentDto.MimeType = detectedType
var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)
if (policyPeriod == null) {
throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
}
if (!_policyAuthCheck.hasAccess(policyPeriod)) {
throw new NoAuthorityException()
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
policyPeriod = bundle.add(policyPeriod)
final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
Plugins.get(IDocumentContentSource).addDocument(new java.io.ByteArrayInputStream(bytes), doc)
return doc
})
/* Should do this outside the transactions as public ID is not set inside the bundle. */
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}Changes to the service request file upload handler
Make these changes in gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs:
package edge.capabilities.servicerequest.document
uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
class ServiceRequestFileUploadHandler extends AbstractRpcHandler {
private var _documentPlugin : IServiceRequestDocumentPlugin
private var _retrievalPlugin : IServiceRequestRetrievalPlugin
private var _fileUploadPlugin: IFileUploadPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _authz : Authorizer<Document>
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("retrievalPlugin", "Plugin used to access service requests")
@Param("fileUploadPugin", "Plugin used to validate file validity")
public construct(documentPlugin : IServiceRequestDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
docAuthz:Authorizer<Document>,
retrievalPlugin : IServiceRequestRetrievalPlugin,
fileUploadPlugin: IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._retrievalPlugin = retrievalPlugin
this._authz = docAuthz
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Uploads a document
*
* Given a list of file items, it expects that the element
* <ul>
* <li> at [0] is the document metadata </li>
* <li> at [1] is the item to be uploaded </li>
* </ul>
*
* </br>
* Throws -
* <ul>
* <li>IllegalArgumentException If id number associated with the document is null or empty</li>
* <li>EntityNotFoundException If no service request statement is found</li>
* <li>AuthorizationException If the portal user has no access to the service request statement</li>
* </ul>
*
* @param list of file items
* @return document metadata response
*/
@JsonRpcMethod
function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)
if (serviceRequest == null) {
throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
serviceRequest = bundle.add(serviceRequest)
var docContent = new DocumentContent(){
:Content = documentFile.get(),
:MimeType = documentDto.MimeType
};
final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
if ( !_authz.canAccess(doc) ) {
throw new NoAuthorityException()
}
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
return doc
})
return _documentPlugin.toDTO(res)
} finally {
documentFile.InputStream.close()
}
}
}package edge.capabilities.servicerequest.document
uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.FileUploadUtil
uses org.apache.tika.Tika
class ServiceRequestFileUploadHandler extends AbstractRpcHandler {
private var _documentPlugin : IServiceRequestDocumentPlugin
private var _retrievalPlugin : IServiceRequestRetrievalPlugin
private var _fileUploadPlugin: IFileUploadPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _authz : Authorizer<Document>
private static final var TIKA = new Tika()
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("retrievalPlugin", "Plugin used to access service requests")
@Param("fileUploadPugin", "Plugin used to validate file validity")
public construct(documentPlugin : IServiceRequestDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
docAuthz:Authorizer<Document>,
retrievalPlugin : IServiceRequestRetrievalPlugin,
fileUploadPlugin: IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._retrievalPlugin = retrievalPlugin
this._authz = docAuthz
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Uploads a document
*
* Given a list of file items, it expects that the element
* <ul>
* <li> at [0] is the document metadata </li>
* <li> at [1] is the item to be uploaded </li>
* </ul>
*
* </br>
* Throws -
* <ul>
* <li>IllegalArgumentException If id number associated with the document is null or empty</li>
* <li>EntityNotFoundException If no service request statement is found</li>
* <li>AuthorizationException If the portal user has no access to the service request statement</li>
* </ul>
*
* @param list of file items
* @return document metadata response
*/
@JsonRpcMethod
function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
// Reject names with control / bidi / non-printable chars
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
FileUploadUtil.enforceMaxUploadSize(documentFile)
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + detectedType)
}
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
documentDto.MimeType = detectedType
var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)
if (serviceRequest == null) {
throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
serviceRequest = bundle.add(serviceRequest)
var docContent = new DocumentContent(){
:Content = bytes,
:MimeType = detectedType
};
final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
if ( !_authz.canAccess(doc) ) {
throw new NoAuthorityException()
}
var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
try {
Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
} finally {
uploadInputStream.close()
}
return doc
})
return _documentPlugin.toDTO(res)
} finally {
documentFile.InputStream.close()
}
}
}New file upload utility
A new FileUploadUtil.gs file has been added:
package edge.security.fileupload
uses java.io.ByteArrayInputStream
uses javax.imageio.ImageIO
uses java.io.IOException
uses javax.imageio.IIOException
uses java.nio.charset.StandardCharsets
uses java.net.URLEncoder
uses java.util.regex.Pattern
uses edge.security.fileupload.exception.IllegalContentTypeException
uses gw.api.util.Logger
uses org.apache.commons.fileupload2.core.FileItem
uses java.io.ByteArrayOutputStream
uses org.apache.tika.Tika
public class FileUploadUtil {
private static final var MAX_UPLOAD_BYTES = 25 * 1024 * 1024 // 25 MB
private static final var LOG = Logger.forCategory(FileUploadUtil.Type.QName)
private static final var TIKA = new Tika()
// Unicode control / bidi / zero-width / non-character ranges that break
// Content-Disposition serialization and enable RTL-override filename spoofing.
// Disallowed at upload; stripped at download for any pre-existing stored names.
// U+061C Arabic Letter Mark (ALM, bidi formatter)
// U+200B-200F zero-width and bidi marks (ZWSP, ZWNJ, ZWJ, LRM, RLM)
// U+202A-202E bidi embedding/override (incl. RLO U+202E)
// U+2066-2069 bidi isolates (LRI, RLI, FSI, PDI -- Trojan Source vectors)
// U+2028-2029 line/paragraph separators
// U+FEFF BOM / zero-width no-break space
// U+FFFD replacement character
// Plus all C0 (0x00-0x1F) and C1 (0x7F-0x9F) control chars.
private static final var DISALLOWED_NAME_CHAR : String =
"[\\u0000-\\u001F\\u007F-\\u009F\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2066-\\u2069\\u2028\\u2029\\uFEFF\\uFFFD]"
private static final var DISALLOWED_NAME_PATTERN : Pattern = Pattern.compile(DISALLOWED_NAME_CHAR)
// XENG-2487: characters that enable Content-Disposition parameter injection
// (`;`), quoted-string breakouts (`"`, `'`), or HTML metachars (`<`, `>`).
// The download-path header builder already escapes these; reject at upload
// as defense in depth.
private static final var HEADER_UNSAFE_CHAR : String = "[\";'<>]"
private static final var HEADER_UNSAFE_PATTERN : Pattern = Pattern.compile(HEADER_UNSAFE_CHAR)
private static final var DEFAULT_DOWNLOAD_FILENAME = "document"
public static function getMaxUploadBytes() : int {
return MAX_UPLOAD_BYTES
}
public static function enforceMaxUploadSize(documentFile : FileItem) {
var size : long
try {
size = documentFile.Size
} catch (e : Exception) {
// Fail closed: a missing/unreadable size indicates a malformed upload that
// we should reject rather than wave through with an unbounded read.
LOG.warn("Rejected upload: could not determine file size: " + e)
throw new IllegalContentTypeException("Uploaded file size could not be determined.")
}
if (size > MAX_UPLOAD_BYTES) {
throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
}
}
public static function readAllBytesAndCloseWithLimit(documentFile : FileItem, maxBytes : int) : byte[] {
var bos = new ByteArrayOutputStream()
var is = documentFile.InputStream
var buffer = new byte[8192]
var total = 0
try {
while (true) {
var read = is.read(buffer)
if (read < 0) {
break
}
total += read
if (total > maxBytes) {
throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
}
bos.write(buffer, 0, read)
}
var bytes = bos.toByteArray()
if (bytes.length == 0) {
throw new IllegalContentTypeException("Uploaded file is empty.")
}
return bytes
} finally {
try {
is.close()
} finally {
bos.close()
}
}
}
public static function normalizeMimeType(mime : String, fileName : String) : String {
var m = mime?.trim()?.toLowerCase()
// Both application/zip and application/x-tika-ooxml are opaque OOXML containers
// at this detection depth. Use the file extension to identify the specific type.
if (m == "application/zip" or m == "application/x-tika-ooxml") {
var mapped = mapZipByExtension(fileName)
if (mapped != null) {
return mapped
}
// No recognized extension — fall through to standard normalization.
// canUploadContentType will reject the unknown type.
}
// application/x-tika-msoffice is Tika's generic supertype for legacy OLE2 binary
// Office formats (.doc, .xls, .ppt, .msg). Palisades upgraded Tika returns this
// instead of the specific canonical MIME types that pre-date OOXML. Use the file
// extension to map back to the canonical type so the allowlist check can match.
if (m == "application/x-tika-msoffice") {
var mapped = mapOleByExtension(fileName)
if (mapped != null) {
return mapped
}
// No recognized extension — fall through; canUploadContentType will reject it.
}
// Tika sniffs CSV bytes as text/plain (the content is plain text), but the
// file extension implies text/csv. Normalize so assertExtensionMatchesType
// and the allowlist check both pass.
if (m == "text/plain") {
var n = fileName?.trim()?.toLowerCase()
if (n != null and n.endsWith(".csv")) {
return "text/csv"
}
}
// video/quicktime is Tika's catch-all for the ISO Base Media File Format (ISOBMFF)
// container family. MP4 files whose ftyp major brand is not one of the specific
// brands Tika has magic bytes for (mp41, mp42) are detected as video/quicktime even
// though they carry a .mp4 extension. Use the file extension to resolve to the
// correct specific type, mirroring the zip→OOXML and msoffice→OLE patterns above.
if (m == "video/quicktime") {
var mapped = mapQuicktimeByExtension(fileName)
if (mapped != null) {
return mapped
}
// .mov / .qt / unrecognised extension — stays video/quicktime.
}
return normalizeMimeType(mime)
}
public static function normalizeMimeType(mime : String) : String {
if (mime == null) {
return null
}
var m = mime.trim().toLowerCase()
if (m == "image/jpg") {
return "image/jpeg"
}
if (m == "application/x-pdf") {
return "application/pdf"
}
// RFC 4337 defines application/mp4 as the generic MP4 container type.
// Tika may return this from extension-based detection while returning video/mp4
// from bytes-based detection (or vice versa). Normalize to video/mp4 so the
// assertExtensionMatchesType comparison and the allowlist check both succeed.
if (m == "application/mp4") {
return "video/mp4"
}
// Tika 3.x canonical type for WAV is audio/vnd.wave; audio/x-wav and audio/wave
// are registered aliases. The allowlist entry is audio/wav (the IANA short form).
// Normalize all three aliases so the allowlist check and extension comparison succeed.
if (m == "audio/vnd.wave" or m == "audio/x-wav" or m == "audio/wave") {
return "audio/wav"
}
// image/x-bmp and image/x-ms-bmp are legacy aliases for image/bmp (IANA-registered
// since RFC 7903). Tika 3.x returns the canonical image/bmp but normalize the aliases
// as defence-in-depth in case a client or older detector path returns one.
if (m == "image/x-bmp" or m == "image/x-ms-bmp") {
return "image/bmp"
}
// application/x-tika-ooxml and application/x-tika-msoffice are intentionally NOT
// resolved here — a filename is required. Use normalizeMimeType(mime, fileName).
return m
}
private static function mapZipByExtension(fileName : String) : String {
if (fileName == null) {
return null
}
var n = fileName.trim().toLowerCase()
if (n.endsWith(".docx")) {
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
if (n.endsWith(".xlsx")) {
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}
if (n.endsWith(".pptx")) {
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
}
return null
}
private static function mapOleByExtension(fileName : String) : String {
if (fileName == null) {
return null
}
var n = fileName.trim().toLowerCase()
if (n.endsWith(".doc") or n.endsWith(".dot")) {
return "application/msword"
}
if (n.endsWith(".xls") or n.endsWith(".xlt") or n.endsWith(".xlm")) {
return "application/vnd.ms-excel"
}
if (n.endsWith(".ppt") or n.endsWith(".pot") or n.endsWith(".pps")) {
return "application/vnd.ms-powerpoint"
}
if (n.endsWith(".msg")) {
return "application/vnd.ms-outlook"
}
return null
}
// Tika detects the QuickTime/ISOBMFF container family as video/quicktime when the
// file's ftyp major brand does not match one of the more specific magic-byte patterns
// (e.g. ftypmp41/mp42 for video/mp4). Use the file extension to resolve to the
// correct allowlisted subtype. .mov and .qt stay as video/quicktime (their own glob
// entries in Tika); anything unrecognised returns null so the caller falls through.
private static function mapQuicktimeByExtension(fileName : String) : String {
if (fileName == null) {
return null
}
var n = fileName.trim().toLowerCase()
if (n.endsWith(".mp4") or n.endsWith(".mp4v") or n.endsWith(".mpg4")) {
return "video/mp4"
}
// .mov and .qt are the native QuickTime extensions — keep video/quicktime.
if (n.endsWith(".mov") or n.endsWith(".qt")) {
return "video/quicktime"
}
return null
}
public static function isDecodableImage(bytes : byte[]) : boolean {
try {
return ImageIO.read(new ByteArrayInputStream(bytes)) != null
} catch (e : IIOException) {
LOG.debug("Image decode failed")
return false
} catch (e : IOException) {
LOG.debug("Image decode failed")
return false
} catch (e : RuntimeException) {
LOG.debug("Image decode failed")
return false
}
}
public static function passesPdfSanityCheck(bytes : byte[]) : boolean {
if (bytes == null or bytes.length < 10) {
return false
}
var headerLen = Math.min(8, bytes.length)
var header = new String(bytes, 0, headerLen, StandardCharsets.ISO_8859_1)
if (!header.startsWith("%PDF-")) {
return false
}
var tailWindow = Math.min(64 * 1024, bytes.length)
var tail = new String(bytes, bytes.length - tailWindow, tailWindow, StandardCharsets.ISO_8859_1)
var eofIndex = tail.lastIndexOf("%%EOF")
if (eofIndex < 0) {
return false
}
var startxrefIndex = tail.lastIndexOf("startxref")
if (startxrefIndex < 0 or startxrefIndex > eofIndex) {
return false
}
var hasClassicXref = tail.contains("\nxref") or tail.contains("\rxref")
var hasClassicTrailer = tail.contains("trailer")
var hasXrefStreamMarker = tail.contains("/XRef")
return (hasClassicXref and hasClassicTrailer) or hasXrefStreamMarker
}
public static function enforcePdfNameConsistency(fileName : String, detectedType : String, mimePdf : String) {
if (fileName == null) {
return
}
var n = fileName.trim().toLowerCase()
var looksLikePdf = n.endsWith(".pdf") or n.contains(".pdf.")
if (looksLikePdf and detectedType != mimePdf) {
LOG.info("Rejected upload: filename indicates PDF but detected content is not PDF")
throw new IllegalContentTypeException("File name indicates a PDF, but the uploaded file is not a valid PDF.")
}
}
public static function validateContent(detectedType : String, bytes : byte[], mimePdf : String, mimePng : String, mimeJpeg : String) {
switch (detectedType) {
case mimePdf:
if (!passesPdfSanityCheck(bytes)) {
LOG.info("Rejected PDF upload: invalid content")
throw new IllegalContentTypeException("The uploaded PDF appears to be invalid or corrupted. Please re-save and try again.")
}
break
case mimePng:
case mimeJpeg:
if (!isDecodableImage(bytes)) {
LOG.info("Rejected image upload: invalid content")
throw new IllegalContentTypeException("The uploaded image appears to be invalid or corrupted. Please re-save and try again.")
}
break
default:
break
}
}
// Reject file names containing Unicode control / bidi / zero-width / non-printable chars.
// These characters break Content-Disposition serialization (causing browsers to render
// attachments inline) and enable RTL-override extension spoofing.
public static function assertSafeFileName(fileName : String) {
if (fileName == null or fileName.length == 0) {
throw new IllegalContentTypeException("File name is required.")
}
var matcher = DISALLOWED_NAME_PATTERN.matcher(fileName)
if (matcher.find()) {
// Report the first offending char as U+XXXX. Never echo the raw character
// into the log or exception message — control/bidi chars can break log
// parsers and enable log-injection.
var codePoint = formatCodePoint(fileName.charAt(matcher.start()) as int)
LOG.info("Rejected upload: file name contains disallowed character " + codePoint)
throw new IllegalContentTypeException(
"File name contains disallowed control or formatting character (" + codePoint + ").")
}
// XENG-2487: reject header-injection / quote-breakout / HTML metachars.
var injMatcher = HEADER_UNSAFE_PATTERN.matcher(fileName)
if (injMatcher.find()) {
var ch = fileName.charAt(injMatcher.start())
LOG.info("Rejected upload: file name contains disallowed character '" + ch + "'")
throw new IllegalContentTypeException("File name must not contain '" + ch + "'.")
}
// Reject path separators so a malicious name cannot influence storage paths.
if (fileName.contains("/") or fileName.contains("\\")) {
LOG.info("Rejected upload: file name contains path separator")
throw new IllegalContentTypeException("File name must not contain path separators.")
}
}
private static function formatCodePoint(code : int) : String {
var hex = Integer.toHexString(code).toUpperCase()
var sb = new StringBuilder("U+")
for (i in 0..|(4 - hex.length)) {
sb.append("0")
}
sb.append(hex)
return sb.toString()
}
// Strip disallowed characters. Used as defense-in-depth on the download path so any
// names stored before the upload-side fix do not break the Content-Disposition header.
public static function sanitizeFileName(fileName : String) : String {
if (fileName == null) {
return DEFAULT_DOWNLOAD_FILENAME
}
var stripped = DISALLOWED_NAME_PATTERN.matcher(fileName).replaceAll("")
.replaceAll("[\\\\/]", "_")
.trim()
return stripped.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : stripped
}
// Build an RFC 6266 Content-Disposition header value: an ASCII-only quoted fallback
// plus a UTF-8 percent-encoded `filename*` for compliant clients. Always returns
// `attachment; ...` so the browser never renders the document inline.
public static function buildContentDisposition(fileName : String) : String {
var safeName = sanitizeFileName(fileName)
var asciiFallback = toAsciiFallback(safeName)
var utf8Encoded = percentEncodeUtf8(safeName)
return "attachment; filename=\"" + asciiFallback + "\"; filename*=UTF-8''" + utf8Encoded
}
// Replace anything outside printable ASCII (0x20-0x7E) with "_" and backslash-escape
// any embedded quote or backslash so the value is safe inside the quoted-string per RFC 6266.
private static function toAsciiFallback(name : String) : String {
var sb = new StringBuilder()
for (i in 0..|name.length) {
var code = (name.charAt(i) as int)
if (code < 0x20 or code > 0x7E) {
sb.append("_")
} else if (code == 0x22 or code == 0x5C) { // " or \
sb.append("\\").append(name.charAt(i))
} else {
sb.append(name.charAt(i))
}
}
var result = sb.toString()
return result.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : result
}
private static function percentEncodeUtf8(name : String) : String {
try {
// URLEncoder follows application/x-www-form-urlencoded rules, which differ
// from RFC 5987 attr-char in two places: spaces become "+" (we convert to
// %20) and "*" is left literal (we percent-encode to %2A so the filename*
// value stays RFC-compliant for filenames like "budget*final.pdf").
return URLEncoder.encode(name, StandardCharsets.UTF_8.name())
.replace("+", "%20")
.replace("*", "%2A")
} catch (e : java.io.UnsupportedEncodingException) {
LOG.debug("UTF-8 encoding unavailable when building Content-Disposition: " + e)
return DEFAULT_DOWNLOAD_FILENAME
}
}
// Cross-check the filename's implied content type (from extension via Tika)
// against the bytes-detected type. Catches the `evil.exe`-with-image-bytes
// pattern where bytes pass the allow-list but the filename extension would
// not. Skips the check when the extension is unknown to Tika so files like
// README (no extension) still go through.
public static function assertExtensionMatchesType(fileName : String, detectedType : String) {
if (fileName == null) {
return
}
var extType : String
try {
extType = TIKA.detect(fileName)
} catch (e : Exception) {
LOG.debug("Could not infer type from filename '" + fileName + "': " + e)
return
}
if (extType == null or extType == "application/octet-stream") {
return
}
var normalizedExt = normalizeMimeType(extType, fileName)
if (normalizedExt != detectedType) {
LOG.info("Rejected upload: filename extension implies " + normalizedExt
+ " but content sniffs as " + detectedType)
throw new IllegalContentTypeException(
"File extension does not match detected content type.")
}
}
}Changes to the claim document upload handler
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in ClaimDocumentUploadHandler.gs:
package edge.capabilities.claim.document
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.capabilities.claim.document.IClaimDocumentPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class ClaimDocumentUploadHandler extends AbstractRpcHandler {
private var _documentPlugin : IClaimDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _claimRetrievalPlugin : IClaimRetrievalPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("claimRetrievalPlugin", "Plugin used to access claim data")
@Param("fileUploadPlugin", "Plugin used to verify file upload validity")
construct(documentPlugin : IClaimDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
claimRetrievalPlugin : IClaimRetrievalPlugin,
fileUploadPlugin : IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._claimRetrievalPlugin = claimRetrievalPlugin
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Used to upload claim documents from the frontend.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
* <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
* <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
* <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
* <dt>Throws:</dt>
* <dd><code>AuthorizationException</code> - If session is invalid</dd>
* <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
* <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
* </dl>
*
* @param documentDto a DTO describing the metadata for the document
* @param documentFile the document itself
* @returns A claimDocumentDTO for display in the frontend
* */
@JsonRpcMethod
@ApidocMethodDescription("Used to upload claim documents from the frontend.")
@ApidocAvailableSince("5.0")
function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch ( ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)
if (claim == null) {
throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
claim = bundle.add(claim)
final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
return doc
})
/* Should do this outside the transactions as public ID is not set inside the bundle. */
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}package edge.capabilities.claim.document
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class ClaimDocumentUploadHandler extends AbstractRpcHandler {
private static final var LOG = Logger.forCategory(ClaimDocumentUploadHandler.Type.QName)
private static final var TIKA = new Tika()
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
private var _documentPlugin : IClaimDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _claimRetrievalPlugin : IClaimRetrievalPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("claimRetrievalPlugin", "Plugin used to access claim data")
@Param("fileUploadPlugin", "Plugin used to verify file upload validity")
construct(documentPlugin : IClaimDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
claimRetrievalPlugin : IClaimRetrievalPlugin,
fileUploadPlugin : IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._claimRetrievalPlugin = claimRetrievalPlugin
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Used to upload claim documents from the frontend.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
* <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
* <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
* <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
* <dt>Throws:</dt>
* <dd><code>AuthorizationException</code> - If session is invalid</dd>
* <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
* <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
* </dl>
*
* @param documentDto a DTO describing the metadata for the document
* @param documentFile the document itself
* @returns A claimDocumentDTO for display in the frontend
* */
@JsonRpcMethod
@ApidocMethodDescription("Used to upload claim documents from the frontend.")
@ApidocAvailableSince("5.0")
function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch ( ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
FileUploadUtil.enforceMaxUploadSize(documentFile)
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
}
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
documentDto.MimeType = detectedType
var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)
if (claim == null) {
throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
claim = bundle.add(claim)
final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
try {
Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
} finally {
uploadInputStream.close()
}
return doc
})
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}Updated package versions
Guidewire has modified the configuration of certain package versions for improved product security. Customers who maintain customized back-end deployments must apply these changes manually.
Jackson databind package update
InsuranceSuite application affected: PolicyCenter, ClaimCenter, BillingCenter
Digital reference application affected: CustomerEngage Account Management, CustomerEngage Account Management for ClaimCenter, CustomerEngage Quote and Buy, ProducerEngage, ProducerEngage for ClaimCenter, ServiceRepEngage, VendorEngage
- Package:
com.fasterxml.jackson.core:jackson-databind - Updated version:
2.18.9 - Files changed: Platform-Java/gosu-jackson-support/pom.xml, pom.xml
Platform-Java/gosu-jackson-support/pom.xml
...
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
- <version>2.18.6</version>
+ <version>2.18.9</version>
<scope>provided</scope>
</dependency>
<dependency>
...
pom.xml
...
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>2.18.9</version>
+ </dependency>
</dependencies>
</dependencyManagement>
...2026.03.1 release
This section lists all of the changes added in patch 1 of the 2026.03 release.
New Jutro version
InsuranceSuite application affected: N/A
Digital reference application affected: All Digital reference applications
Digital reference applications now use Jutro patch version 10.13.2. This
brings enhanced security to the Jutro Design System.
ProducerEngage gateway account API endpoint changes
InsuranceSuite application affected: PolicyCenter
Digital reference application affected: ProducerEngage
Guidewire has modified the configuration of the ProducerEngage
/gateway/account API endpoint for improved security. There are steps that
you can take to have improved security and need to manually add the changes to your own
files.
After making this change, ServiceRepEngage users must ensure that they have the correct producer codes assigned to their account in PolicyCenter in order to create quotes.
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make the following changes to /gsrc/edge/capabilities/gateway/account/AccountHandler.gs:
Original file content:
@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Returns or creates and returns the given account.")
@ApidocAvailableSince("4.0")
function getOrCreateAccount(anAccountDTO: AccountDTO): AccountDTO {
if (anAccountDTO.AccountNumber == null) {
final var newAccount = _accountPlugin.createAccount(anAccountDTO)
return _accountPlugin.accountBaseDetailsToDTO(newAccount)
}
final var account = _accountRetrievalPlugin.getAccountByNumber(anAccountDTO.AccountNumber)
return _accountPlugin.accountBaseDetailsToDTO(account)
}
Updated file content:
@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Returns or creates and returns the given account.")
@ApidocAvailableSince("4.0")
function getOrCreateAccount(anAccountDTO: AccountDTO): AccountDTO {
if (anAccountDTO.AccountNumber == null) {
verifyProducerCodeOwnership(anAccountDTO)
final var newAccount = _accountPlugin.createAccount(anAccountDTO)
return _accountPlugin.accountBaseDetailsToDTO(newAccount)
}
final var account = _accountRetrievalPlugin.getAccountByNumber(anAccountDTO.AccountNumber)
return _accountPlugin.accountBaseDetailsToDTO(account)
}
private function verifyProducerCodeOwnership(anAccountDTO: AccountDTO) {
if (anAccountDTO.ProducerCodes == null || !anAccountDTO.ProducerCodes.HasElements) {
return
}
final var ownedCodes = User.util.CurrentUser.UserProducerCodes*.ProducerCode*.Code.toSet()
final var unauthorized = anAccountDTO.ProducerCodes.firstWhere(\pc -> !ownedCodes.contains(pc.Code))
if (unauthorized != null) {
throw new AuthorizationException() {:Message = "User is not authorized to create accounts under producer code: " + unauthorized.Code}
}
}
ProducerEngage API search result changes
InsuranceSuite application affected: PolicyCenter
Digital reference application affected: ProducerEngage
Guidewire has modified the configuration of the ProducerEngage API search response for improved security. There are steps that you can take to have improved security and need to manually add the changes to your own files.
- /gsrc/edge/capabilities/gateway/account/AccountHandler.gs
- /gsrc/edge/capabilities/gateway/account/DefaultAccountPlugin.gs
- /gsrc/edge/capabilities/gateway/account/IAccountPlugin.gs
- /gsrc/edge/capabilities/gateway/account/dto/AccountSearchHolderDTO.gs
- /gsrc/edge/capabilities/gateway/account/dto/AccountSearchResultDTO.gs
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix.
AccountHandler changes
Make the following changes to /gsrc/edge/capabilities/gateway/account/AccountHandler.gs:
package edge.capabilities.gateway.account
uses edge.jsonrpc.AbstractRpcHandler
uses edge.capabilities.gateway.account.dto.AccountJobsSummaryDTO
uses edge.aspects.validation.annotations.Context
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.capabilities.gateway.account.search.dto.AccountSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.IAccountSearchPlugin
uses edge.capabilities.gateway.job.submission.dto.ProductSelectionDTO
uses edge.capabilities.gateway.job.submission.dto.NewSubmissionDTO
uses edge.capabilities.gateway.job.submission.ISubmissionPlugin
uses edge.capabilities.gateway.note.dto.NoteDTO
uses edge.capabilities.gateway.note.INotePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.document.IDocumentPlugin
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.capabilities.gateway.billing.dto.BillingInvoiceDTO
uses edge.capabilities.gateway.billing.IPolicyPeriodBillingSummaryPlugin
uses edge.capabilities.gateway.billing.dto.PolicyPeriodBillingSummaryDTO
uses java.lang.Exception
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.permission.IPermissionCheckPlugin
uses java.lang.SecurityException
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.capabilities.gateway.claim.dto.ClaimSummaryDTO
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.jsonrpc.exception.JsonRpcInternalErrorException
uses edge.exception.EntityNotFoundException
uses edge.capabilities.gateway.account.dto.AccountSummaryDTO
uses edge.capabilities.gateway.account.dto.AccountJobsDTO
uses java.lang.Integer
uses edge.capabilities.gateway.job.dto.JobSummaryDTO
uses edge.capabilities.gateway.job.IJobSummaryPlugin
uses edge.capabilities.gateway.billing.IAccountBillingPlugin
uses edge.capabilities.gateway.billing.dto.AccountBillingDTO
uses edge.capabilities.gateway.account.dto.PolicyTransactionSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.dto.AccountSearchSummaryDTO
uses gw.api.database.Query
uses edge.PlatformSupport.Bundle
uses edge.capabilities.helpers.pagination.dto.QueryOptionsDTO
uses edge.capabilities.helpers.pagination.dto.QueryParameterDTO
uses java.util.ArrayList@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Returns a list of potentially existing accounts.")
@ApidocAvailableSince("4.0")
function getPotentialExistingAccounts(accountSearchCriteriaDTO: AccountSearchCriteriaDTO): AccountDTO[] {
if(!perm.System.searchaccounts) {
throw new AuthorizationException(){:Message = "User is not authorized to search accounts"}
}
var anAccountSearchCriteria = _accountSearchPlugin.createSearchCriteria(accountSearchCriteriaDTO)
if (accountSearchCriteriaDTO.ContactType == ContactType.TC_COMPANY) {
/*If company name is greater than 5 characters then do an exact match*/
anAccountSearchCriteria.CompanyNameExact = (anAccountSearchCriteria.CompanyName.length < 5)
} else if (accountSearchCriteriaDTO.ContactType == ContactType.TC_PERSON) {
anAccountSearchCriteria.LastNameExact = (anAccountSearchCriteria.LastName.length < 5)
anAccountSearchCriteria.FirstNameExact = (anAccountSearchCriteria.FirstName.length < 5)
}
final var potentialAccounts = anAccountSearchCriteria.performSearch()*.Account
return _accountPlugin.toDTOArray(potentialAccounts)
}package edge.capabilities.gateway.account
uses edge.jsonrpc.AbstractRpcHandler
uses edge.capabilities.gateway.account.dto.AccountJobsSummaryDTO
uses edge.aspects.validation.annotations.Context
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.capabilities.gateway.account.search.dto.AccountSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.IAccountSearchPlugin
uses edge.capabilities.gateway.job.submission.dto.ProductSelectionDTO
uses edge.capabilities.gateway.job.submission.dto.NewSubmissionDTO
uses edge.capabilities.gateway.job.submission.ISubmissionPlugin
uses edge.capabilities.gateway.note.dto.NoteDTO
uses edge.capabilities.gateway.note.INotePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.document.IDocumentPlugin
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.capabilities.gateway.billing.dto.BillingInvoiceDTO
uses edge.capabilities.gateway.billing.IPolicyPeriodBillingSummaryPlugin
uses edge.capabilities.gateway.billing.dto.PolicyPeriodBillingSummaryDTO
uses java.lang.Exception
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.permission.IPermissionCheckPlugin
uses java.lang.SecurityException
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.capabilities.gateway.claim.dto.ClaimSummaryDTO
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.jsonrpc.exception.JsonRpcInternalErrorException
uses edge.exception.EntityNotFoundException
uses edge.capabilities.gateway.account.dto.AccountSummaryDTO
uses edge.capabilities.gateway.account.dto.AccountJobsDTO
uses java.lang.Integer
uses edge.capabilities.gateway.job.dto.JobSummaryDTO
uses edge.capabilities.gateway.job.IJobSummaryPlugin
uses edge.capabilities.gateway.billing.IAccountBillingPlugin
uses edge.capabilities.gateway.billing.dto.AccountBillingDTO
uses edge.capabilities.gateway.account.dto.PolicyTransactionSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.dto.AccountSearchSummaryDTO
uses edge.capabilities.gateway.account.dto.AccountSearchResultDTO
uses gw.api.database.Query
uses edge.PlatformSupport.Bundle
uses edge.capabilities.helpers.pagination.dto.QueryOptionsDTO
uses edge.capabilities.helpers.pagination.dto.QueryParameterDTO
uses java.util.ArrayList@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Returns a list of potentially existing accounts.")
@ApidocAvailableSince("4.0")
function getPotentialExistingAccounts(accountSearchCriteriaDTO: AccountSearchCriteriaDTO): AccountSearchResultDTO[] {
if(!perm.System.searchaccounts) {
throw new AuthorizationException(){:Message = "User is not authorized to search accounts"}
}
var anAccountSearchCriteria = _accountSearchPlugin.createSearchCriteria(accountSearchCriteriaDTO)
if (accountSearchCriteriaDTO.ContactType == ContactType.TC_COMPANY) {
/*If company name is greater than 5 characters then do an exact match*/
anAccountSearchCriteria.CompanyNameExact = (anAccountSearchCriteria.CompanyName.length < 5)
} else if (accountSearchCriteriaDTO.ContactType == ContactType.TC_PERSON) {
anAccountSearchCriteria.LastNameExact = (anAccountSearchCriteria.LastName.length < 5)
anAccountSearchCriteria.FirstNameExact = (anAccountSearchCriteria.FirstName.length < 5)
}
final var potentialAccounts = anAccountSearchCriteria.performSearch()*.Account
return _accountPlugin.toSearchResultDTOArray(potentialAccounts)
}DefaultAccount plugin changes
Make the following changes to /gsrc/edge/capabilities/gateway/account/DefaultAccountPlugin.gs:
package edge.capabilities.gateway.account
uses edge.PlatformSupport.CurrencyPlatformUtil
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.capabilities.gateway.contact.IContactPlugin
uses edge.capabilities.gateway.currency.local.ICurrencyPlugin
uses edge.capabilities.gateway.policy.IPolicySummaryPlugin
uses edge.di.annotations.ForAllGwNodes
uses java.lang.IllegalArgumentException
uses java.util.Date
uses edge.capabilities.gateway.billing.IAccountBillingSummaryPlugin
uses edge.capabilities.gateway.contact.dto.ContactBaseDTO
uses java.lang.Exception
uses edge.PlatformSupport.Reflection
uses edge.PlatformSupport.Logger
uses edge.capabilities.gateway.user.local.IProducerCodePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.exception.EntityNotFoundException
uses gw.pl.currency.MonetaryAmount
uses gw.product.ProducerCodeSearchCriteria
uses edge.capabilities.policycommon.accountcontact.IAccountContactPluginoverride function toDTOArray(accounts: Account[]): AccountDTO[] {
if (accounts != null && !accounts.IsEmpty){
return accounts.map(\acc -> toDTO(acc))
}
return new AccountDTO[]{}
}
override function updateAccount(anAccount: Account, dto: AccountDTO) {
var accountHolder: ContactBaseDTO = null
if (dto.AccountHolder != null) {
_accContactPlugin.updateContact(anAccount.AccountHolderContact, dto.AccountHolder)
}
}package edge.capabilities.gateway.account
uses edge.PlatformSupport.CurrencyPlatformUtil
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.capabilities.gateway.account.dto.AccountSearchResultDTO
uses edge.capabilities.gateway.account.dto.AccountSearchHolderDTO
uses edge.capabilities.address.dto.AddressDTO
uses edge.capabilities.gateway.contact.IContactPlugin
uses edge.capabilities.gateway.currency.local.ICurrencyPlugin
uses edge.capabilities.gateway.policy.IPolicySummaryPlugin
uses edge.di.annotations.ForAllGwNodes
uses java.lang.IllegalArgumentException
uses java.util.Date
uses edge.capabilities.gateway.billing.IAccountBillingSummaryPlugin
uses edge.capabilities.gateway.contact.dto.ContactBaseDTO
uses java.lang.Exception
uses edge.PlatformSupport.Reflection
uses edge.PlatformSupport.Logger
uses edge.capabilities.gateway.user.local.IProducerCodePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.exception.EntityNotFoundException
uses gw.pl.currency.MonetaryAmount
uses gw.product.ProducerCodeSearchCriteria
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin override function toDTOArray(accounts: Account[]): AccountDTO[] {
if (accounts != null && !accounts.IsEmpty){
return accounts.map(\acc -> toDTO(acc))
}
return new AccountDTO[]{}
}
override function toSearchResultDTOArray(accounts: Account[]): AccountSearchResultDTO[] {
if (accounts == null || accounts.IsEmpty) {
return new AccountSearchResultDTO[]{}
}
return accounts.map(\acc -> {
final var dto = new AccountSearchResultDTO()
dto.AccountNumber = acc.AccountNumber
final var holder = new AccountSearchHolderDTO()
holder.DisplayName = acc.AccountHolderContact?.DisplayName
final var contactAddress = acc.AccountHolderContact?.PrimaryAddress
if (contactAddress != null) {
final var addr = new AddressDTO()
addr.AddressLine1 = contactAddress.AddressLine1
addr.City = contactAddress.City
addr.State = contactAddress.State
addr.PostalCode = contactAddress.PostalCode
addr.Country = contactAddress.Country
holder.PrimaryAddress = addr
}
dto.AccountHolder = holder
return dto
})
}
override function updateAccount(anAccount: Account, dto: AccountDTO) {
var accountHolder: ContactBaseDTO = null
if (dto.AccountHolder != null) {
_accContactPlugin.updateContact(anAccount.AccountHolderContact, dto.AccountHolder)
}
}IAccount plugin changes
Make the following changes to /gsrc/edge/capabilities/gateway/account/IAccountPlugin.gs:
package edge.capabilities.gateway.account
uses edge.capabilities.gateway.account.dto.AccountDTO
interface IAccountPlugin {
public function toDTO(anAccount : Account) : AccountDTO
public function toDTOArray(accounts : Account[]) : AccountDTO[]
public function accountBaseDetailsToDTO(anAccount : Account) : AccountDTO
public function accountBaseDetailsToDTOArray(accounts : Account[]) : AccountDTO[]
public function updateAccount(anAccount : Account, dto : AccountDTO)
public function createAccount(dto : AccountDTO) : Account
}package edge.capabilities.gateway.account
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.capabilities.gateway.account.dto.AccountSearchResultDTO
interface IAccountPlugin {
public function toDTO(anAccount : Account) : AccountDTO
public function toDTOArray(accounts : Account[]) : AccountDTO[]
public function accountBaseDetailsToDTO(anAccount : Account) : AccountDTO
public function accountBaseDetailsToDTOArray(accounts : Account[]) : AccountDTO[]
public function toSearchResultDTOArray(accounts : Account[]) : AccountSearchResultDTO[]
public function updateAccount(anAccount : Account, dto : AccountDTO)
public function createAccount(dto : AccountDTO) : Account
}AccountSearchHolderDTO changes
package edge.capabilities.gateway.account.dto
uses edge.jsonmapper.JsonProperty
uses edge.capabilities.address.dto.AddressDTO
/**
* Minimal account-holder summary for search results.
* Contains only display name and primary address — no PII (DOB, licence, email, phone).
*/
class AccountSearchHolderDTO {
@JsonProperty
var _displayName : String as DisplayName
@JsonProperty
var _primaryAddress : AddressDTO as PrimaryAddress
}AccountSearchResultDTO changes
package edge.capabilities.gateway.account.dto
uses edge.jsonmapper.JsonProperty
/**
* Minimal DTO for account search results. Contains only the fields required to identify
* an account in a list — account number and a stripped-down account holder (display name
* and primary address only). Sensitive fields (DOB, licence, email, phone, billing data,
* activity counts) are intentionally excluded.
*/
class AccountSearchResultDTO {
@JsonProperty
var _accountNumber : String as AccountNumber
@JsonProperty
var _accountHolder : AccountSearchHolderDTO as AccountHolder
}CustomerEngage Quote and Buy API endpoint changes
InsuranceSuite application affected: PolicyCenter
Digital reference application affected: CustomerEngage Quote and Buy
Guidewire has modified the configuration of the CustomerEngage Quote and Buy
/guidance/guidance API endpoint for improved security. There
are steps that you can take to have improved security and need to manually add the
changes to your own files.
- UserProfileHandler.gs
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix.
UserProfileHandler changes
Make the following changes to /gsrc/edge/capabilities/profileinfo/user/UserProfileHandler.gs:
@JsonRpcMethod
@ApidocMethodDescription("When the user wishes to update their account contact information.")
@ApidocAvailableSince("5.0")
public function updateAccountContactSummary(newAccountSummaryDTO : AccountSummaryDTO) {
final var account = AccountUtil.getAccountByAccountNumber(newAccountSummaryDTO.AccountNumber)
AccountUtil.verifyAccountForEffectiveUser(_userProvider.EffectiveUser, account);
var accountContact = newAccountSummaryDTO.AccountContact
_accountPlugin.updateAccountContactSummary(account, accountContact)
if(newAccountSummaryDTO.isBillingAddressSame) {
_accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.AccountContact)
} else {
_accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.BillingContact)
}
}
}@JsonRpcMethod
@ApidocMethodDescription("When the user wishes to update their account contact information.")
@ApidocAvailableSince("5.0")
public function updateAccountContactSummary(newAccountSummaryDTO : AccountSummaryDTO) {
final var account = AccountUtil.getAccountByAccountNumber(newAccountSummaryDTO.AccountNumber, _userProvider.EffectiveUser)
var accountContact = newAccountSummaryDTO.AccountContact
_accountPlugin.updateAccountContactSummary(account, accountContact)
if(newAccountSummaryDTO.isBillingAddressSame) {
_accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.AccountContact)
} else {
_accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.BillingContact)
}
}
}CustomerEngage Account Management improved access control
InsuranceSuite application affected: PolicyCenter
Digital reference application affected: CustomerEngage Account Management
- /gsrc/edge/capabilities/quote/submission/GatewayQuoteHandler.gs
- /gsrc/edge/capabilities/quote/helper/session/DefaultSessionPlugin.gs
- /gsrc/edge/capabilities/quote/helper/session/ISessionPlugin.gs
- /gsrc/edge/capabilities/quote/submission/QuoteHandler.gs
- /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteRetrievalHandler.gs
- /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteHandler.gs
- /gsrc/edge/oauth/authplugin/EdgeAuthenticationSourceCreatorPlugin.gs
- /gsrc/main/gsrc/edge/servlet/security/DefaultHttpRequestUserIdentityPlugin.gs
- applications/common/capabilities-react/gw-capability-quoteandbind-ho-react/HOWizard.jsx
- applications/common/capabilities-react/gw-capability-quoteandbind-pa-react/PAWizard.jsx
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix.
GatewayQuoteHandler changes
The following changes need to be made to /gsrc/edge/capabilities/quote/submission/GatewayQuoteHandler.gs:
package edge.capabilities.quote.submission
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO
class GatewayQuoteHandler extends QuoteHandler{
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
construct(
basePlugin : IBaseSubmissionPlugin,
lobPlugin : IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin : ILobCoveragesPlugin <LobCoveragesDTO>,
sessionPlugin : ISessionPlugin,
quotingPlugin : ISubmissionQuotePlugin,
bindingPlugin : IBindingPlugin,
quoteMailingPlugin : IQuoteMailingPlugin,
lobMetadataPlugin : ILobMetadataPlugin,
authorizer:IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin) {
super(
basePlugin,
lobPlugin,
lobCoveragesUpdatePlugin,
sessionPlugin,
quotingPlugin,
bindingPlugin,
quoteMailingPlugin,
lobMetadataPlugin,
authorizer,
accContactPlugin,
validationPlugin,
aUWIssuePlugin
)package edge.capabilities.quote.submission
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.security.EffectiveUserProvider
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO
class GatewayQuoteHandler extends QuoteHandler{
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
@Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
construct(
basePlugin : IBaseSubmissionPlugin,
lobPlugin : IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin : ILobCoveragesPlugin <LobCoveragesDTO>,
sessionPlugin : ISessionPlugin,
quotingPlugin : ISubmissionQuotePlugin,
bindingPlugin : IBindingPlugin,
quoteMailingPlugin : IQuoteMailingPlugin,
lobMetadataPlugin : ILobMetadataPlugin,
authorizer:IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin,
aUserProvider: EffectiveUserProvider) {
super(
basePlugin,
lobPlugin,
lobCoveragesUpdatePlugin,
sessionPlugin,
quotingPlugin,
bindingPlugin,
quoteMailingPlugin,
lobMetadataPlugin,
authorizer,
accContactPlugin,
validationPlugin,
aUWIssuePlugin,
aUserProvider
)DefaultSession plugin changes
The following changes need to be made to /gsrc/edge/capabilities/quote/helper/session/DefaultSessionPlugin.gs:
override function getSession(foreignId: String): String {
final var sess = Bundle.resolveInTransaction(\bundle -> {
final var mtSession = new PortalSession_MPExt()
mtSession.foreignId = foreignId
mtSession.sessionType = DEFAULT_SESSION_TYPE
mtSession.sessionUUID = UUID.randomUUID().toString()
mtSession.issueDate = DateUtil.currentDate()
return mtSession
})
return sess.sessionUUID
}override function getSession(foreignId: String): String {
return getSession(foreignId, null)
}
override function getSession(foreignId: String, callerUsername: String): String {
final var sess = Bundle.resolveInTransaction(\bundle -> {
final var mtSession = new PortalSession_MPExt()
mtSession.foreignId = foreignId
mtSession.sessionType = DEFAULT_SESSION_TYPE
mtSession.sessionUUID = UUID.randomUUID().toString()
mtSession.issueDate = DateUtil.currentDate()
mtSession.username = callerUsername
return mtSession
})
return sess.sessionUUID
}ISession plugin changes
The following changes need to be made to /gsrc/edge/capabilities/quote/helper/session/ISessionPlugin.gs:
package edge.capabilities.quote.helper.session
uses java.lang.String
uses gw.lang.Returns
/**
* Service used to work with quote sessions.
*/
interface ISessionPlugin {
function getSession(foreignId : String):String
/**
* Validates and refreshes a session.
*/
@Returns("<code>true</code> iff session was valid (session timeout should be updaded)." +
"<code>false</code> iff session is not valid")
function validateAndRefreshSession(sessionUUID : String, foreignId : String)
}package edge.capabilities.quote.helper.session
uses java.lang.String
uses gw.lang.Returns
/**
* Service used to work with quote sessions.
*/
interface ISessionPlugin {
function getSession(foreignId : String):String
/**
* Creates a session bound to a specific caller identity.
* The callerUsername is stored so it can be verified at retrieval time.
* Passing null is allowed (anonymous callers).
*/
function getSession(foreignId : String, callerUsername : String) : String
/**
* Validates and refreshes a session.
*/
@Returns("<code>true</code> iff session was valid (session timeout should be updaded)." +
"<code>false</code> iff session is not valid")
function validateAndRefreshSession(sessionUUID : String, foreignId : String)
}QuoteHandler changes
The following changes need to be made to /gsrc/edge/capabilities/quote/submission/QuoteHandler.gs:
package edge.capabilities.quote.submission
uses edge.jsonrpc.annotation.CaptchaCheck
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.availability.ProductCodeUtil
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.AbstractRpcHandler
uses edge.PlatformSupport.Bundle
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses gw.api.database.Query
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.authorization.Authorizer
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.helper.questionset.util.QuestionSetUtil
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.webapimodel.dto.PCWebApiModelDTO
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses java.lang.IllegalArgumentException
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.submission.util.SubmissionUtil
uses gw.util.Pair
class QuoteHandler extends AbstractRpcHandler {
private static final var LOGGER = new Logger(Reflection.getRelativeName(QuoteHandler))
/**
* Validation Rules Plugin
*/
protected var _validationPlugin : IEdgeValidationRulesPlugin
/**
* Used to map underwriting issues and approve/refer issues if permissions allow.
*/
protected var _uwIssuePlugin: IUWIssuePlugin
/**
* A plugin to manage account contacts
*/
private var _accContactPlugin: IAccountContactPlugin
/**
* Base submission plugin used in quote.
*/
private var _basePlugin: IBaseSubmissionPlugin
/**
* Plugin to process LOB specific data.
*/
protected var _lobPlugin: IQuoteLobDataPlugin<LobDTO>
/**
* Plugin to process LOB coverage data.
*/
private var _lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>
/**
* Session management plugin.
*/
protected var _sessionPlugin: ISessionPlugin
/**
* Quoting plugin.
*/
protected var _quotingPlugin: ISubmissionQuotePlugin
/**
* Submission binding plugin.
*/
private var _bindingPlugin: IBindingPlugin
/**
* Quote mailing plugin.
*/
private var _quoteMailingPlugin: IQuoteMailingPlugin
/**
* Metadata generation extension plugin.
*/
private var _lobMetadataPlugin: ILobMetadataPlugin
/**
* Authorizer
*/
private var _submissionAuthorizer: Authorizer<Submission>as readonly SubmissionAuthorizer
construct() { }
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
construct(
basePlugin: IBaseSubmissionPlugin,
lobPlugin: IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
sessionPlugin: ISessionPlugin,
quotingPlugin: ISubmissionQuotePlugin,
bindingPlugin: IBindingPlugin,
quoteMailingPlugin: IQuoteMailingPlugin,
lobMetadataPlugin: ILobMetadataPlugin,
authorizer: IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin
) {
this._basePlugin = basePlugin
this._lobPlugin = lobPlugin
this._lobCoveragesUpdatePlugin = lobCoveragesUpdatePlugin
this._sessionPlugin = sessionPlugin
this._quotingPlugin = quotingPlugin
this._bindingPlugin = bindingPlugin
this._quoteMailingPlugin = quoteMailingPlugin
this._lobMetadataPlugin = lobMetadataPlugin
this._submissionAuthorizer = authorizer.authorizerFor(Submission)
this._accContactPlugin = accContactPlugin
this._validationPlugin = validationPlugin
this._uwIssuePlugin = aUWIssuePlugin
}
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Retrieves the meta data required for the quoting handler, including details such as display keys and question sets.")
@ApidocAvailableSince("5.0")
function getMetaData(): Object {
return PCWebApiModelDTO.forTypes(
{QuoteDataDTO, QuoteEmailDTO, QuoteRetrievalDTO},
_lobMetadataPlugin.getQuestionSetCodes().map(\qs -> QuestionSetUtil.getQuestionSetByCode(qs))
)
}
/**
* Creates new submission job and generates a session to be used during the submission process.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>IBaseSubmissionPlugin#createSubmission(String,QuoteBaseDataDTO)</code> -
* to create a new submission passing the base data in <code>qdd.BaseData</code></dd>
* <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
* <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
* returned dto</dd>
* <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
* <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
* </dl>
*
* @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
* and <code>qdd.Lobs</code>
* @return the DTO for the newly created submission. The contents of the returned value are as follows:
* <dl>
* <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
* <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
* <dt>BaseData</dt><dd>data common across LOBs</dd>
* <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
* <dt>QuotingData</dt><dd><code>null</code></dd>
* <dt>BindingData</dt><dd><code>null</code></dd>
* <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
* </dl>
*/
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Creates new submission job and generates a session to be used during the submission process.")
@ApidocAvailableSince("4.0")
public function create(qdd: QuoteDataDTO): QuoteDataDTO {
final var draftSubmission = Bundle.resolveInTransaction(\b -> {
final var versionSpecificProductCode = ProductCodeUtil.getVersionSpecificProductCode(qdd.BaseData.ProductCode)
var aSubmission = _basePlugin.createSubmission(versionSpecificProductCode, qdd.BaseData)
_lobPlugin.updateFromDTO(aSubmission.SelectedVersion, qdd.LobData, true)
return aSubmission
})
final var sessId = _sessionPlugin.getSession(draftSubmission.JobNumber)
return toDTOBaseData(sessId, draftSubmission)
}
/**
* Creates new submission job for an account and generates a session to be used during the submission process. It does not update
* the account details.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>create(qdd : QuoteDataDTO)</code> -
* to create a new submission passing the data in <code>qdd</code>. </dd>
* <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
* <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
* returned dto</dd>
* <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
* <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
* </dl>
*
* @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
* and <code>qdd.Lobs</code>
* @return the DTO for the newly created submission. The contents of the returned value are as follows:
* <dl>
* <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
* <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
* <dt>BaseData</dt><dd>draft data common across LOBs</dd>
* <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
* <dt>QuotingData</dt><dd><code>null</code></dd>
* <dt>BindingData</dt><dd><code>null</code></dd>
* <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
* </dl>
*/
@JsonRpcMethod
@ApidocMethodDescription("Creates new submission job for an account and generates a session to be used during the submission process. It does not update the account detail.")
@ApidocAvailableSince("4.0")
public function createForAccount(qdd: QuoteDataDTO): QuoteDataDTO {
if (qdd.BaseData == null || qdd.BaseData.AccountNumber == null) {
throw new IllegalArgumentException("Account Number is required.")
}
return Bundle.resolveInTransaction(\bundle -> {
var existingAccount = bundle.add(Account.finder.findAccountByAccountNumber(qdd.BaseData.AccountNumber))
qdd.BaseData.AccountHolder = _accContactPlugin.toDTO(existingAccount.AccountHolderContact)
return create(qdd)
})
}/**
* Retrieves an account submission
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>retrieve(QuoteRetrievalDTO)</code> - to retrieve the account submission</dd>
* <dt>Throws:</dt>
* <dd><code>EntityNotFoundException</code> - if no submission can be found with a matching postal code</dd>
* </dl>
*
* @param qrd data that captures the information needed to retrieve a quote
* @return the DTO for the updated submission. The contents of the returned value are as follows:
* <dl>
* <dt>SessionUUID</dt><dd>the session ID</dd>
* <dt>QuoteID</dt><dd>the JobNumber of the submission</dd>
* <dt>BaseData</dt><dd>draft data common across LOBs</dd>
* <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
* <dt>QuotingData</dt><dd>the quote data</dd>
* <dt>BindingData</dt><dd>the bind data</dd>
* <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
* </dl>
*/
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Retrieves an account submission")
@ApidocAvailableSince("2021.11.0")
public function retrieveAccountSubmission(qrd: QuoteRetrievalDTO): QuoteDataDTO {
final var DEFAULT_EXPIRY_HOURS: int = 1
final var DEFAULT_SESSION_TYPE: String = "Submission"
if (!qrd.sessionUUID.HasContent) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
final var results = Query.make(PortalSession_MPExt).compare("sessionUUID", Equals, qrd.sessionUUID).select()
if(results.Count != 1) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
final var session = results.FirstResult
if (!DEFAULT_SESSION_TYPE.equals(session.sessionType)) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
if (!gw.api.util.DateUtil.addHours(session.issueDate, DEFAULT_EXPIRY_HOURS).after( Date.CurrentDate)) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
return this.retrieve(qrd)
}package edge.capabilities.quote.submission
uses edge.jsonrpc.annotation.CaptchaCheck
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.availability.ProductCodeUtil
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.AbstractRpcHandler
uses edge.PlatformSupport.Bundle
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses gw.api.database.Query
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.authorization.Authorizer
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.helper.questionset.util.QuestionSetUtil
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.webapimodel.dto.PCWebApiModelDTO
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses java.lang.IllegalArgumentException
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.submission.util.SubmissionUtil
uses edge.security.EffectiveUserProvider
uses edge.security.authorization.AuthorityType
uses gw.util.Pair
class QuoteHandler extends AbstractRpcHandler {
private static final var LOGGER = new Logger(Reflection.getRelativeName(QuoteHandler))
/**
* Validation Rules Plugin
*/
protected var _validationPlugin : IEdgeValidationRulesPlugin
/**
* Used to map underwriting issues and approve/refer issues if permissions allow.
*/
protected var _uwIssuePlugin: IUWIssuePlugin
/**
* A plugin to manage account contacts
*/
private var _accContactPlugin: IAccountContactPlugin
/**
* Base submission plugin used in quote.
*/
private var _basePlugin: IBaseSubmissionPlugin
/**
* Plugin to process LOB specific data.
*/
protected var _lobPlugin: IQuoteLobDataPlugin<LobDTO>
/**
* Plugin to process LOB coverage data.
*/
private var _lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>
/**
* Session management plugin.
*/
protected var _sessionPlugin: ISessionPlugin
/**
* Quoting plugin.
*/
protected var _quotingPlugin: ISubmissionQuotePlugin
/**
* Submission binding plugin.
*/
private var _bindingPlugin: IBindingPlugin
/**
* Quote mailing plugin.
*/
private var _quoteMailingPlugin: IQuoteMailingPlugin
/**
* Metadata generation extension plugin.
*/
private var _lobMetadataPlugin: ILobMetadataPlugin
/**
* Authorizer
*/
private var _submissionAuthorizer: Authorizer<Submission>as readonly SubmissionAuthorizer
/**
* Provider for the effective (JWT-authenticated) user, used to verify account ownership.
*/
protected var _userProvider: EffectiveUserProvider
construct() { }
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
@Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
construct(
basePlugin: IBaseSubmissionPlugin,
lobPlugin: IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
sessionPlugin: ISessionPlugin,
quotingPlugin: ISubmissionQuotePlugin,
bindingPlugin: IBindingPlugin,
quoteMailingPlugin: IQuoteMailingPlugin,
lobMetadataPlugin: ILobMetadataPlugin,
authorizer: IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin,
aUserProvider: EffectiveUserProvider
) {
this._basePlugin = basePlugin
this._lobPlugin = lobPlugin
this._lobCoveragesUpdatePlugin = lobCoveragesUpdatePlugin
this._sessionPlugin = sessionPlugin
this._quotingPlugin = quotingPlugin
this._bindingPlugin = bindingPlugin
this._quoteMailingPlugin = quoteMailingPlugin
this._lobMetadataPlugin = lobMetadataPlugin
this._submissionAuthorizer = authorizer.authorizerFor(Submission)
this._accContactPlugin = accContactPlugin
this._validationPlugin = validationPlugin
this._uwIssuePlugin = aUWIssuePlugin
this._userProvider = aUserProvider
}
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Retrieves the meta data required for the quoting handler, including details such as display keys and question sets.")
@ApidocAvailableSince("5.0")
function getMetaData(): Object {
return PCWebApiModelDTO.forTypes(
{QuoteDataDTO, QuoteEmailDTO, QuoteRetrievalDTO},
_lobMetadataPlugin.getQuestionSetCodes().map(\qs -> QuestionSetUtil.getQuestionSetByCode(qs))
)
}
/**
* Creates new submission job and generates a session to be used during the submission process.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>IBaseSubmissionPlugin#createSubmission(String,QuoteBaseDataDTO)</code> -
* to create a new submission passing the base data in <code>qdd.BaseData</code></dd>
* <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
* <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
* returned dto</dd>
* <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
* <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
* </dl>
*
* @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
* and <code>qdd.Lobs</code>
* @return the DTO for the newly created submission. The contents of the returned value are as follows:
* <dl>
* <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
* <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
* <dt>BaseData</dt><dd>data common across LOBs</dd>
* <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
* <dt>QuotingData</dt><dd><code>null</code></dd>
* <dt>BindingData</dt><dd><code>null</code></dd>
* <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
* </dl>
*/
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Creates new submission job and generates a session to be used during the submission process.")
@ApidocAvailableSince("4.0")
public function create(qdd: QuoteDataDTO): QuoteDataDTO {
final var draftSubmission = Bundle.resolveInTransaction(\b -> {
final var versionSpecificProductCode = ProductCodeUtil.getVersionSpecificProductCode(qdd.BaseData.ProductCode)
var aSubmission = _basePlugin.createSubmission(versionSpecificProductCode, qdd.BaseData)
_lobPlugin.updateFromDTO(aSubmission.SelectedVersion, qdd.LobData, true)
return aSubmission
})
// Sessions created at quote creation time are not bound to a specific caller because
// they may be created by an agent on behalf of a customer, or shared via URL.
// Caller binding is applied later when the customer resumes the quote via retrieve().
final var sessId = _sessionPlugin.getSession(draftSubmission.JobNumber)
return toDTOBaseData(sessId, draftSubmission)
}
/**
* Creates new submission job for an account and generates a session to be used during the submission process. It does not update
* the account details.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>create(qdd : QuoteDataDTO)</code> -
* to create a new submission passing the data in <code>qdd</code>. </dd>
* <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
* <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
* returned dto</dd>
* <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
* <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
* </dl>
*
* @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
* and <code>qdd.Lobs</code>
* @return the DTO for the newly created submission. The contents of the returned value are as follows:
* <dl>
* <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
* <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
* <dt>BaseData</dt><dd>draft data common across LOBs</dd>
* <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
* <dt>QuotingData</dt><dd><code>null</code></dd>
* <dt>BindingData</dt><dd><code>null</code></dd>
* <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
* </dl>
*/
@JsonRpcMethod
@ApidocMethodDescription("Creates new submission job for an account and generates a session to be used during the submission process. It does not update the account detail.")
@ApidocAvailableSince("4.0")
public function createForAccount(qdd: QuoteDataDTO): QuoteDataDTO {
if (qdd.BaseData == null || qdd.BaseData.AccountNumber == null) {
throw new IllegalArgumentException("Account Number is required.")
}
final var result = Bundle.resolveInTransaction(\bundle -> {
var existingAccount = bundle.add(Account.finder.findAccountByAccountNumber(qdd.BaseData.AccountNumber))
qdd.BaseData.AccountHolder = _accContactPlugin.toDTO(existingAccount.AccountHolderContact)
return create(qdd)
})
// Bind the newly-created session to the account number in the DB so that
// retrieveAccountSubmission can perform a server-side (non-forgeable) ownership check.
Bundle.transaction(\bundle -> {
final var sessResults = Query.make(PortalSession_MPExt).compare("sessionUUID", Equals, result.SessionUUID).select()
if (sessResults.Count == 1) {
final var sess = bundle.add(sessResults.FirstResult)
sess.username = qdd.BaseData.AccountNumber
}
})
return result
}/**
* Retrieves an account submission
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>retrieve(QuoteRetrievalDTO)</code> - to retrieve the account submission</dd>
* <dt>Throws:</dt>
* <dd><code>EntityNotFoundException</code> - if no submission can be found with a matching postal code</dd>
* </dl>
*
* @param qrd data that captures the information needed to retrieve a quote
* @return the DTO for the updated submission. The contents of the returned value are as follows:
* <dl>
* <dt>SessionUUID</dt><dd>the session ID</dd>
* <dt>QuoteID</dt><dd>the JobNumber of the submission</dd>
* <dt>BaseData</dt><dd>draft data common across LOBs</dd>
* <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
* <dt>QuotingData</dt><dd>the quote data</dd>
* <dt>BindingData</dt><dd>the bind data</dd>
* <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
* </dl>
*/
@JsonRpcMethod
@ApidocMethodDescription("Retrieves an account submission")
@ApidocAvailableSince("2021.11.0")
public function retrieveAccountSubmission(qrd: QuoteRetrievalDTO): QuoteDataDTO {
final var effectiveUser = _userProvider.EffectiveUser
final var DEFAULT_EXPIRY_HOURS: int = 1
final var DEFAULT_SESSION_TYPE: String = "Submission"
if (!qrd.sessionUUID.HasContent) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
final var results = Query.make(PortalSession_MPExt).compare("sessionUUID", Equals, qrd.sessionUUID).select()
if(results.Count != 1) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
final var session = results.FirstResult
// Require a JWT with an ACCOUNT authority that matches this session's account number.
// The QnB frontend always sends Authorization: Bearer for logged-in users, so
// effectiveUser carries grantedAuthorities (e.g. "guidewire.edge.account.C000143542.all").
// Access is denied when:
// - the caller is not authenticated (no JWT → no account authorities), or
// - the caller's JWT account does not match the session's account (cross-account URL attack).
if (session.username.HasContent) {
final var jwtAccountNumbers = effectiveUser?.getTargets(AuthorityType.ACCOUNT)
if (jwtAccountNumbers == null || jwtAccountNumbers.isEmpty()
|| !jwtAccountNumbers.contains(session.username)) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
}
if (!session.foreignId.equals(qrd.QuoteID)) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
if (!DEFAULT_SESSION_TYPE.equals(session.sessionType)) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
if (!gw.api.util.DateUtil.addHours(session.issueDate, DEFAULT_EXPIRY_HOURS).after( Date.CurrentDate)) {
throw new JsonRpcSecurityException(){:Message = "Invalid session"}
}
return this.retrieve(qrd)
}UnderwritingQuoteRetrievalHandler changes
The following changes need to be made to /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteRetrievalHandler.gs
package edge.capabilities.quote.submission
uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingPaymentPlanPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin
class UnderwritingQuoteRetrievalHandler extends UnderwritingQuoteHandler {
var _paymentPlanPlugin: IUnderwritingPaymentPlanPlugin
construct() {
}
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
@Param("industryCodePlugin", "Plugin used for retrieving industry codes")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
construct(
basePlugin: IBaseSubmissionPlugin,
underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
lobPlugin: IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
sessionPlugin: ISessionPlugin,
quotingPlugin: ISubmissionQuotePlugin,
bindingPlugin: IBindingPlugin,
quoteMailingPlugin: IQuoteMailingPlugin,
lobMetadataPlugin: ILobMetadataPlugin,
authorizer: IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
underwritingBindingPlugin: IUnderwritingBindingPlugin,
issuePlugin: IIssuePlugin,
policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
quoteDocumentPlugin: IQuoteDocumentPlugin,
industryCodePlugin: IIndustryCodePlugin,
paymentPlanPlugin: IUnderwritingPaymentPlanPlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin
) {
super(basePlugin, underwritingBasePlugin, lobPlugin, lobCoveragesUpdatePlugin, sessionPlugin, quotingPlugin, bindingPlugin, quoteMailingPlugin,
lobMetadataPlugin, authorizer, accContactPlugin, underwritingBindingPlugin, issuePlugin, policyPeriodRetrievalPlugin, submissionRetrievalPlugin,
policyPeriodPlugin, quoteDocumentPlugin, industryCodePlugin, validationPlugin, aUWIssuePlugin)
_paymentPlanPlugin = paymentPlanPlugin
}
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Retrieves a submission.")
@ApidocAvailableSince("10.0 UM-PC Integration")
public function retrieve(qrd: QuoteRetrievalDTO): QuoteDataDTO {
var res = super.retrieve(qrd)
var submission = getSubmissionByJob(res.QuoteID)
var boundPeriod = submission.ResultingBoundPeriod
if (boundPeriod != null) {
_paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(res.BindData.PaymentPlans.first(), boundPeriod)
}
return res
}
/**
* Retrieves the payment plans for the current submission
*
* @param quoteID quoteID of submission that needs to be found
* @param sessionId current session id
* @return a list of existing payment plans for the given Submission
*/
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Retrieves the payment plans for the given submission.")
@ApidocAvailableSince("10.0 UM-PC Integration")
public function retrievePaymentPlans(quoteID: String, sessionId: String): PaymentPlanDTO[] {
var paymentPlans = super.retrievePaymentPlans(quoteID, sessionId)
var submission = getSubmissionByJob(quoteID)
var selectedVersion = submission.SelectedVersion
if (selectedVersion != null) {
paymentPlans.each(\plan ->
_paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(plan, selectedVersion)
)
}
return paymentPlans
}
}package edge.capabilities.quote.submission
uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingPaymentPlanPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.EffectiveUserProvider
class UnderwritingQuoteRetrievalHandler extends UnderwritingQuoteHandler {
var _paymentPlanPlugin: IUnderwritingPaymentPlanPlugin
construct() {
}
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
@Param("industryCodePlugin", "Plugin used for retrieving industry codes")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
@Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
construct(
basePlugin: IBaseSubmissionPlugin,
underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
lobPlugin: IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
sessionPlugin: ISessionPlugin,
quotingPlugin: ISubmissionQuotePlugin,
bindingPlugin: IBindingPlugin,
quoteMailingPlugin: IQuoteMailingPlugin,
lobMetadataPlugin: ILobMetadataPlugin,
authorizer: IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
underwritingBindingPlugin: IUnderwritingBindingPlugin,
issuePlugin: IIssuePlugin,
policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
quoteDocumentPlugin: IQuoteDocumentPlugin,
industryCodePlugin: IIndustryCodePlugin,
paymentPlanPlugin: IUnderwritingPaymentPlanPlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin,
aUserProvider: EffectiveUserProvider
) {
super(basePlugin, underwritingBasePlugin, lobPlugin, lobCoveragesUpdatePlugin, sessionPlugin, quotingPlugin, bindingPlugin, quoteMailingPlugin,
lobMetadataPlugin, authorizer, accContactPlugin, underwritingBindingPlugin, issuePlugin, policyPeriodRetrievalPlugin, submissionRetrievalPlugin,
policyPeriodPlugin, quoteDocumentPlugin, industryCodePlugin, validationPlugin, aUWIssuePlugin, aUserProvider)
_paymentPlanPlugin = paymentPlanPlugin
}
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Retrieves a submission.")
@ApidocAvailableSince("10.0 UM-PC Integration")
public function retrieve(qrd: QuoteRetrievalDTO): QuoteDataDTO {
var res = super.retrieve(qrd)
var submission = getSubmissionByJob(res.QuoteID)
var boundPeriod = submission.ResultingBoundPeriod
if (boundPeriod != null) {
_paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(res.BindData.PaymentPlans.first(), boundPeriod)
}
return res
}
/**
* Retrieves the payment plans for the current submission
*
* @param quoteID quoteID of submission that needs to be found
* @param sessionId current session id
* @return a list of existing payment plans for the given Submission
*/
@JsonRpcUnauthenticatedMethod
@ApidocMethodDescription("Retrieves the payment plans for the given submission.")
@ApidocAvailableSince("10.0 UM-PC Integration")
public function retrievePaymentPlans(quoteID: String, sessionId: String): PaymentPlanDTO[] {
var paymentPlans = super.retrievePaymentPlans(quoteID, sessionId)
var submission = getSubmissionByJob(quoteID)
var selectedVersion = submission.SelectedVersion
if (selectedVersion != null) {
paymentPlans.each(\plan ->
_paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(plan, selectedVersion)
)
}
return paymentPlans
}
}
UnderwritingQuoteHandler changes
The following changes need to be made to /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteHandler.gs:
package edge.capabilities.quote.submission
uses edge.PlatformSupport.Bundle
uses edge.aspects.validation.annotations.Context
uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.industrycode.dto.IndustryCodeDTO
uses edge.capabilities.industrycode.dto.IndustryCodeSearchCriteriaDTO
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.IssuanceDTO
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteDocumentDTO
uses edge.capabilities.quote.submission.dto.QuoteOnlyRequestDTO
uses edge.capabilities.quote.submission.dto.UnderwritingQuoteDataDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionRequestDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionResponseDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin
uses java.lang.IllegalArgumentException
uses java.lang.IllegalStateException
uses gw.api.privacy.EncryptionMaskExpressions
class UnderwritingQuoteHandler extends QuoteHandler {
var _underwritingBindingPlugin: IUnderwritingBindingPlugin
var _issuePlugin: IIssuePlugin
var _policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin
var _underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin
var _submissionRetrievalPlugin: ISubmissionRetrievalPlugin
var _policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin
var _quoteDocumentPlugin: IQuoteDocumentPlugin
var _industryCodePlugin: IIndustryCodePlugin
construct() {
}
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
@Param("industryCodePlugin", "Plugin used for retrieving industry codes")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
construct(
basePlugin: IBaseSubmissionPlugin,
underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
lobPlugin: IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
sessionPlugin: ISessionPlugin,
quotingPlugin: ISubmissionQuotePlugin,
bindingPlugin: IBindingPlugin,
quoteMailingPlugin: IQuoteMailingPlugin,
lobMetadataPlugin: ILobMetadataPlugin,
authorizer: IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
underwritingBindingPlugin: IUnderwritingBindingPlugin,
issuePlugin: IIssuePlugin,
policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
quoteDocumentPlugin: IQuoteDocumentPlugin,
industryCodePlugin: IIndustryCodePlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin) {
super(
basePlugin,
lobPlugin,
lobCoveragesUpdatePlugin,
sessionPlugin,
quotingPlugin,
bindingPlugin,
quoteMailingPlugin,
lobMetadataPlugin,
authorizer,
accContactPlugin,
validationPlugin,
aUWIssuePlugin)
_underwritingBindingPlugin = underwritingBindingPlugin
_issuePlugin = issuePlugin
_policyPeriodRetrievalPlugin = policyPeriodRetrievalPlugin
_underwritingBasePlugin = underwritingBasePlugin
_submissionRetrievalPlugin = submissionRetrievalPlugin
_policyPeriodPlugin = policyPeriodPlugin
_quoteDocumentPlugin = quoteDocumentPlugin
_industryCodePlugin = industryCodePlugin
}package edge.capabilities.quote.submission
uses edge.PlatformSupport.Bundle
uses edge.aspects.validation.annotations.Context
uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.industrycode.dto.IndustryCodeDTO
uses edge.capabilities.industrycode.dto.IndustryCodeSearchCriteriaDTO
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.IssuanceDTO
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteDocumentDTO
uses edge.capabilities.quote.submission.dto.QuoteOnlyRequestDTO
uses edge.capabilities.quote.submission.dto.UnderwritingQuoteDataDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionRequestDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionResponseDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.EffectiveUserProvider
uses java.lang.IllegalArgumentException
uses java.lang.IllegalStateException
uses gw.api.privacy.EncryptionMaskExpressions
class UnderwritingQuoteHandler extends QuoteHandler {
var _underwritingBindingPlugin: IUnderwritingBindingPlugin
var _issuePlugin: IIssuePlugin
var _policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin
var _underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin
var _submissionRetrievalPlugin: ISubmissionRetrievalPlugin
var _policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin
var _quoteDocumentPlugin: IQuoteDocumentPlugin
var _industryCodePlugin: IIndustryCodePlugin
construct() {
}
@InjectableNode
@Param("basePlugin", "Plugin used to manage base quote information")
@Param("lobPlugin", "Plugin used to manage lob specific information")
@Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
@Param("sessionPlugin", "Session management plugin")
@Param("quotingPlugin", "Quoting process plugin implementation")
@Param("bindingPlugin", "Binding process plugin")
@Param("quoteMailingPlugin", "Plugin used to mail the quote")
@Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
@Param("authorizer", "Authorizer added to check user can retrieve submission")
@Param("accContactPlugin", "Plugin used for creating and updating the account holder")
@Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
@Param("industryCodePlugin", "Plugin used for retrieving industry codes")
@Param("validationPlugin", "Plugin used to perform entity validation")
@Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
@Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
construct(
basePlugin: IBaseSubmissionPlugin,
underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
lobPlugin: IQuoteLobDataPlugin<LobDTO>,
lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
sessionPlugin: ISessionPlugin,
quotingPlugin: ISubmissionQuotePlugin,
bindingPlugin: IBindingPlugin,
quoteMailingPlugin: IQuoteMailingPlugin,
lobMetadataPlugin: ILobMetadataPlugin,
authorizer: IAuthorizerProviderPlugin,
accContactPlugin: IAccountContactPlugin,
underwritingBindingPlugin: IUnderwritingBindingPlugin,
issuePlugin: IIssuePlugin,
policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
quoteDocumentPlugin: IQuoteDocumentPlugin,
industryCodePlugin: IIndustryCodePlugin,
validationPlugin : IEdgeValidationRulesPlugin,
aUWIssuePlugin : IUWIssuePlugin,
aUserProvider: EffectiveUserProvider) {
super(
basePlugin,
lobPlugin,
lobCoveragesUpdatePlugin,
sessionPlugin,
quotingPlugin,
bindingPlugin,
quoteMailingPlugin,
lobMetadataPlugin,
authorizer,
accContactPlugin,
validationPlugin,
aUWIssuePlugin,
aUserProvider)
_underwritingBindingPlugin = underwritingBindingPlugin
_issuePlugin = issuePlugin
_policyPeriodRetrievalPlugin = policyPeriodRetrievalPlugin
_underwritingBasePlugin = underwritingBasePlugin
_submissionRetrievalPlugin = submissionRetrievalPlugin
_policyPeriodPlugin = policyPeriodPlugin
_quoteDocumentPlugin = quoteDocumentPlugin
_industryCodePlugin = industryCodePlugin
}EdgeAuthenticationSourceCreator plugin changes
The following changes need to be made to /gsrc/edge/oauth/authplugin/EdgeAuthenticationSourceCreatorPlugin.gs:
package edge.oauth.authplugin
uses gw.auth.gwhub.AuthSourceCreatorPlugin
uses gw.plugin.security.AuthenticationSource
uses jakarta.servlet.http.HttpServletRequest
uses gw.plugin.security.UserNamePasswordAuthenticationSource
uses java.nio.charset.Charset
uses gw.util.Base64Util
uses gw.plugin.security.InvalidAuthenticationSourceData
uses com.nimbusds.jwt.JWTClaimsSet
uses java.lang.Exception
uses edge.servlet.jsonrpc.JsonRpcUnauthenticatedServlet
uses edge.oauth.authplugin.OAuthAuthenticationSource
class EdgeAuthenticationSourceCreatorPlugin extends AuthSourceCreatorPlugin {
private static final var OAUTH_ATTR_NAME = "OAuthSource"
private static final var GW_USER_CONTEXT = "GWUserContext"
override function createSourceFromHTTPRequest(req: HttpServletRequest): AuthenticationSource {
if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
return new AnonymousAuthenticationSource()
}
var source : AuthenticationSource
source = getOAuthAuthenticationSource(req)
if (source == null) {
source = this.getUserNamePasswordAuthenticationSource(req)
}
if (source == null) {
source = super.createSourceFromHTTPRequest(req)
}
return source
}
private function getOAuthAuthenticationSource(req: HttpServletRequest): OAuthAuthenticationSource {
var authHeader = req.getHeader("Authorization")
var accessToken : String
if (authHeader != null && authHeader.indexOf("Bearer") != -1) {
accessToken = authHeader.substring("Bearer".length())?.trim()
}
if (req.ParameterNames.toList().contains("access_token")) {
accessToken = req.getParameter("access_token")
}
if(accessToken?.NotBlank) {
var jwtPlugin = new JwtVerificationPlugin()
var claimsSet: JWTClaimsSet = null
try {
claimsSet = jwtPlugin.verifyToken(accessToken)
} catch (e: Exception) {
throw new InvalidAuthenticationSourceData(e)
}
var userContextBase64 = req.getHeader(GW_USER_CONTEXT as String)
var userContext : String
var oAuthSource : OAuthAuthenticationSource
if (userContextBase64 != null) {
userContext = new String(Base64Util.decode(userContextBase64), Charset.forName("UTF-8"))
}
oAuthSource = new OAuthAuthenticationSource(claimsSet, userContext)
req.setAttribute(OAUTH_ATTR_NAME, oAuthSource)
return oAuthSource
}
return null
}
...package edge.oauth.authplugin
uses gw.auth.gwhub.AuthSourceCreatorPlugin
uses gw.plugin.security.AuthenticationSource
uses jakarta.servlet.http.HttpServletRequest
uses gw.plugin.security.UserNamePasswordAuthenticationSource
uses java.nio.charset.Charset
uses gw.util.Base64Util
uses gw.plugin.security.InvalidAuthenticationSourceData
uses com.nimbusds.jwt.JWTClaimsSet
uses java.lang.Exception
uses edge.servlet.jsonrpc.JsonRpcUnauthenticatedServlet
uses edge.oauth.authplugin.OAuthAuthenticationSource
class EdgeAuthenticationSourceCreatorPlugin extends AuthSourceCreatorPlugin {
private static final var OAUTH_ATTR_NAME = "OAuthSource"
private static final var GW_USER_CONTEXT = "GWUserContext"
override function createSourceFromHTTPRequest(req: HttpServletRequest): AuthenticationSource {
if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
// Even on the unauthenticated servlet path, attempt to extract and store the JWT
// as an OAuthAuthenticationSource attribute. This allows the EffectiveUserProvider
// to be populated with the caller's JWT identity (including ACCOUNT authority claims)
// for ownership checks, without changing the PC user resolution (still anonymous).
getOAuthAuthenticationSource(req)
return new AnonymousAuthenticationSource()
}
var source : AuthenticationSource
source = getOAuthAuthenticationSource(req)
if (source == null) {
source = this.getUserNamePasswordAuthenticationSource(req)
}
if (source == null) {
source = super.createSourceFromHTTPRequest(req)
}
return source
}
private function getOAuthAuthenticationSource(req: HttpServletRequest): OAuthAuthenticationSource {
var authHeader = req.getHeader("Authorization")
var accessToken : String
if (authHeader != null && authHeader.indexOf("Bearer") != -1) {
accessToken = authHeader.substring("Bearer".length())?.trim()
}
if (req.ParameterNames.toList().contains("access_token")) {
accessToken = req.getParameter("access_token")
}
if(accessToken?.NotBlank) {
var jwtPlugin = new JwtVerificationPlugin()
var claimsSet: JWTClaimsSet = null
try {
claimsSet = jwtPlugin.verifyToken(accessToken)
} catch (e: Exception) {
throw new InvalidAuthenticationSourceData(e)
}
var userContextBase64 = req.getHeader(GW_USER_CONTEXT as String)
var userContext : String
if (userContextBase64 != null) {
userContext = new String(Base64Util.decode(userContextBase64), Charset.forName("UTF-8"))
}
var oAuthSource = new OAuthAuthenticationSource(claimsSet, userContext)
req.setAttribute(OAUTH_ATTR_NAME, oAuthSource)
return oAuthSource
}
return null
}
...DefaultHttpRequestUserIdentity plugin changes
The following changes need to be made to /gsrc/edge/servlet/security/DefaultHttpRequestUserIdentityPlugin.gs:
class DefaultHttpRequestUserIdentityPlugin implements IHttpRequestUserIdentityPlugin {
private static final var OAUTH_ATTR_NAME = "OAuthSource"
private static final var GW_USER_CONTEXT = "GWUserContext"
@ForAllGwNodes
construct() {
}
override function getEffectiveUserFromRequest(req : HttpServletRequest) : EffectiveUser {
var effectiveUser : EffectiveUser
var hasOAuthSource = req.getAttribute(OAUTH_ATTR_NAME) != null
if (hasOAuthSource) {
return getEffectiveUserFromOAuthSource(req)
}
if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
//anonymous access
effectiveUser = new EffectiveUser(null, null, null, null)
} else {
var currentUser = User.util.CurrentUser
//Internal user logging in with username and password
effectiveUser = new EffectiveUser(currentUser.Credential.UserName, null, currentUser, null)
}
return effectiveUser
}
private function getEffectiveUserFromOAuthSource(req : HttpServletRequest) : EffectiveUser {
final var currentUser = User.util.CurrentUser
final var accessToken = getAccessToken(req)
final var oAuthSource = req.getAttribute(OAUTH_ATTR_NAME) as OAuthAuthenticationSource
final var userContext = req.getHeader(GW_USER_CONTEXT)
var userId = getUserId(oAuthSource.Claims)
if (oAuthSource.IsInternalUser) {
//Internal user logging in with jwt token issued by xcenter
return new EffectiveUser(oAuthSource.Username, null, currentUser, userId, accessToken)
}
if (oAuthSource.IsServiceUser) {
var user = oAuthSource.serviceUser
if (oAuthSource.IsInternalUserForThisApplication) {
if (user == null) {
throw new IllegalArgumentException("Access token user identifier not found in request header")
}
}
return new EffectiveUser(user, null, currentUser, userId, accessToken, userContext)
}
if (oAuthSource.HasServiceUserAuthorities) {
if (oAuthSource.serviceUserAuthorities == null) {
throw new IllegalArgumentException("Access token user authorities not found in request header")
}
var grantedAuthorities = getGrantedAuthorities(oAuthSource.Claims, oAuthSource.serviceUserAuthorities)
return new EffectiveUser(oAuthSource.Username, grantedAuthorities, null, userId, accessToken, userContext)
}class DefaultHttpRequestUserIdentityPlugin implements IHttpRequestUserIdentityPlugin {
private static final var OAUTH_ATTR_NAME = "OAuthSource"
private static final var GW_USER_CONTEXT = "GWUserContext"
@ForAllGwNodes
construct() {
}
override function getEffectiveUserFromRequest(req : HttpServletRequest) : EffectiveUser {
var effectiveUser : EffectiveUser
var hasOAuthSource = req.getAttribute(OAUTH_ATTR_NAME) != null
if (hasOAuthSource) {
return getEffectiveUserFromOAuthSource(req)
}
if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
//anonymous access
effectiveUser = new EffectiveUser(null, null, null, null)
} else {
var currentUser = User.util.CurrentUser
//Internal user logging in with username and password (no prior JWT on this session)
effectiveUser = new EffectiveUser(currentUser.Credential.UserName, null, currentUser, null)
}
return effectiveUser
}
private function getEffectiveUserFromOAuthSource(req : HttpServletRequest) : EffectiveUser {
final var currentUser = User.util.CurrentUser
final var accessToken = getAccessToken(req)
final var oAuthSource = req.getAttribute(OAUTH_ATTR_NAME) as OAuthAuthenticationSource
final var userContext = req.getHeader(GW_USER_CONTEXT)
var userId = getUserId(oAuthSource.Claims)
if (oAuthSource.IsInternalUser) {
//Internal user logging in with jwt token issued by xcenter.
// Also parse grantedAuthorities from the JWT when present (e.g. portal users
// who have a pcUserName claim AND account-scoped authorities in the same token).
var internalGrantedAuthorities = oAuthSource.Claims.Claims.get("grantedAuthorities") != null
? getGrantedAuthorities(oAuthSource.Claims, null)
: null
return new EffectiveUser(oAuthSource.Username, internalGrantedAuthorities, currentUser, userId, accessToken)
}
if (oAuthSource.IsServiceUser) {
var user = oAuthSource.serviceUser
if (oAuthSource.IsInternalUserForThisApplication) {
if (user == null) {
throw new IllegalArgumentException("Access token user identifier not found in request header")
}
}
// Parse grantedAuthorities from the JWT when present — service/portal users
// may have account-scoped authorities alongside the service-user scope.
var serviceGrantedAuthorities = oAuthSource.Claims.Claims.get("grantedAuthorities") != null
? getGrantedAuthorities(oAuthSource.Claims, null)
: null
return new EffectiveUser(user, serviceGrantedAuthorities, currentUser, userId, accessToken, userContext)
}
if (oAuthSource.HasServiceUserAuthorities) {
if (oAuthSource.serviceUserAuthorities == null) {
throw new IllegalArgumentException("Access token user authorities not found in request header")
}
var grantedAuthorities = getGrantedAuthorities(oAuthSource.Claims, oAuthSource.serviceUserAuthorities)
return new EffectiveUser(oAuthSource.Username, grantedAuthorities, null, userId, accessToken, userContext)
}HOWizard changes
The following changes need to be made to applications/common/capabilities-react/gw-capability-quoteandbind-ho-react/HOWizard.jsx:
function HOWizard(props) {
const {
showConfirm,
showAlert
} = useModal();
const { steps, title } = wizardConfig;
const [initialSubmission, setInitialSubmission] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
const viewModelService = useContext(ViewModelServiceContext);
const { location, history } = props;
useEffect(() => {
const viewModelContext = {
AccountEmailRequired: true,
AccountDOBRequired: true
};
let submissionPromise;
if (!viewModelService) {
if (_.isEmpty(location.search)) {
history.push('/');
}
return;
}
if (_.has(location, 'state.address')) {
const { address } = location.state;
submissionPromise = createSubmissionOnPartialAddress(address);
} else if (_.has(location, 'state.submission')) {
const { submission } = location.state;
setShouldSkipValidSteps(true);
submissionPromise = Promise.resolve(submission);
} else if (_.has(location, 'search')) {
const parsedParms = queryString.parse(location.search);
submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms);
} else {
return;
}
submissionPromise.then((response) => {
const submissionVM = viewModelService.create(
removeMockData(response),
'pc',
'edge.capabilities.quote.submission.dto.QuoteDataDTO',
viewModelContext
);
setInitialSubmission(submissionVM);
setIsLoading(false);
});
// only execute this once per component lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewModelService]);
...function HOWizard(props) {
const {
showConfirm,
showAlert
} = useModal();
const { steps, title } = wizardConfig;
const [initialSubmission, setInitialSubmission] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
const viewModelService = useContext(ViewModelServiceContext);
const { location, history } = props;
const [qnbAuthHeader] = useState(() => {
// sessionStorage survives StrictMode's double-invocation of state initializers.
// First invocation: reads hash, stores token, clears hash (result discarded by StrictMode).
// Second invocation: reads from sessionStorage, removes it, returns the token.
const stored = sessionStorage.getItem('qnbToken');
if (stored) {
sessionStorage.removeItem('qnbToken');
return { Authorization: `Bearer ${stored}` };
}
const fragment = window.location.hash;
if (fragment) {
const hashParams = new URLSearchParams(fragment.slice(1));
const token = hashParams.get('t');
if (token) {
const decoded = decodeURIComponent(token);
window.history.replaceState({}, '', window.location.pathname + window.location.search);
sessionStorage.setItem('qnbToken', decoded);
return { Authorization: `Bearer ${decoded}` };
}
}
return {};
});
useEffect(() => {
const viewModelContext = {
AccountEmailRequired: true,
AccountDOBRequired: true
};
let submissionPromise;
if (!viewModelService) {
if (_.isEmpty(location.search)) {
history.push('/');
}
return;
}
if (_.has(location, 'state.address')) {
const { address } = location.state;
submissionPromise = createSubmissionOnPartialAddress(address);
} else if (_.has(location, 'state.submission')) {
const { submission } = location.state;
setShouldSkipValidSteps(true);
submissionPromise = Promise.resolve(submission);
} else if (_.has(location, 'search')) {
const parsedParms = queryString.parse(location.search);
submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms, qnbAuthHeader);
} else {
return;
}
submissionPromise
.then((response) => {
const submissionVM = viewModelService.create(
removeMockData(response),
'pc',
'edge.capabilities.quote.submission.dto.QuoteDataDTO',
viewModelContext
);
setInitialSubmission(submissionVM);
setIsLoading(false);
})
.catch(() => {
setIsLoading(false);
history.push('/contact-us');
});
// only execute this once per component lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewModelService]);
...PAWizard changes
The following changes need to be made to applications/common/capabilities-react/gw-capability-quoteandbind-pa-react/PAWizard.jsx:
function createSubmissionOnPartialAddress(address) {
const data = setPrepData(address);
return LoadSaveService.createSubmission(data);
}
function PAWizard(props) {
const {
showConfirm,
showAlert
} = useModal();
const { steps, title } = wizardConfig;
const [initialSubmission, setInitialSubmission] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
const viewModelService = useContext(ViewModelServiceContext);
const { location, history } = props;
useEffect(() => {
const viewModelContext = {
AccountEmailRequired: true,
DriverEmailRequired: true,
AccountDOBRequired: true
};
if (!viewModelService) {
if (_.isEmpty(location.search)) {
history.push('/');
}
return;
}
let submissionPromise;
if (_.has(location, 'state.address')) {
const { address } = location.state;
submissionPromise = createSubmissionOnPartialAddress(address);
} else if (_.has(location, 'state.submission')) {
const { submission } = location.state;
setShouldSkipValidSteps(true);
submissionPromise = Promise.resolve(submission);
} else if (_.has(location, 'search')) {
const parsedParms = queryString.parse(location.search);
submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms);
} else {
return;
}
submissionPromise.then((response) => {
const submissionVM = viewModelService.create(
removeMockData(response),
'pc',
'edge.capabilities.quote.submission.dto.QuoteDataDTO',
viewModelContext
);
setInitialSubmission(submissionVM);
setIsLoading(false);
});
// only execute this once per component lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewModelService]);
...function createSubmissionOnPartialAddress(address) {
const data = setPrepData(address);
return LoadSaveService.createSubmission(data);
}
function PAWizard(props) {
const {
showConfirm,
showAlert
} = useModal();
const { steps, title } = wizardConfig;
const [initialSubmission, setInitialSubmission] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
const viewModelService = useContext(ViewModelServiceContext);
const { location, history } = props;
const [qnbAuthHeader] = useState(() => {
// sessionStorage survives StrictMode's double-invocation of state initializers.
// First invocation: reads hash, stores token, clears hash (result discarded by StrictMode).
// Second invocation: reads from sessionStorage, removes it, returns the token.
const stored = sessionStorage.getItem('qnbToken');
if (stored) {
sessionStorage.removeItem('qnbToken');
return { Authorization: `Bearer ${stored}` };
}
const fragment = window.location.hash;
if (fragment) {
const hashParams = new URLSearchParams(fragment.slice(1));
const token = hashParams.get('t');
if (token) {
const decoded = decodeURIComponent(token);
window.history.replaceState({}, '', window.location.pathname + window.location.search);
sessionStorage.setItem('qnbToken', decoded);
return { Authorization: `Bearer ${decoded}` };
}
}
return {};
});
useEffect(() => {
const viewModelContext = {
AccountEmailRequired: true,
DriverEmailRequired: true,
AccountDOBRequired: true
};
if (!viewModelService) {
if (_.isEmpty(location.search)) {
history.push('/');
}
return;
}
let submissionPromise;
if (_.has(location, 'state.address')) {
const { address } = location.state;
submissionPromise = createSubmissionOnPartialAddress(address);
} else if (_.has(location, 'state.submission')) {
const { submission } = location.state;
setShouldSkipValidSteps(true);
submissionPromise = Promise.resolve(submission);
} else if (_.has(location, 'search')) {
const parsedParms = queryString.parse(location.search);
submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms, qnbAuthHeader);
}
submissionPromise
.then((response) => {
const submissionVM = viewModelService.create(
removeMockData(response),
'pc',
'edge.capabilities.quote.submission.dto.QuoteDataDTO',
viewModelContext
);
setInitialSubmission(submissionVM);
setIsLoading(false);
})
.catch(() => {
setIsLoading(false);
history.push('/contact-us');
});
// only execute this once per component lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewModelService]);
...Updated package version
InsuranceSuite application affected: PolicyCenter
Digital reference application affected: ProducerEngage
Guidewire has modified the configuration of the JDK18on package version for improved product security. Customers who maintain customized back-end deployments must apply this change manually.
JDK18on package update
- Package:
org.bouncycastle:bcprov-jdk18on - Updated version:
1.84 - File changed: Capabilities/gpa-policy/pom.xml
- Change
made:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> - <version>1.79</version> + <version>1.84</version> </dependency>
2026.03 release
This section lists all of the changes added in the 2026.03 release.
New Jutro version
InsuranceSuite application affected: N/A
Digital reference application affected: All Digital reference applications
Digital reference applications now use Jutro 10.13.0.
New Jutro Cloud Standards package
The Jutro 10.13.0 release introduces a new Jutro Cloud Standards package. This package provides Stylelint and ESLint configurations that validate your application against Cloud Standards. Cloud Standards help ensure that your application uses only public Jutro APIs and does not violate framework guidelines. You might see additional warnings when you run applications locally or in TeamCity, but the warnings won't block your builds.
For more information, see Cloud Standards - Use of Jutro APIs.
Updated Lerna version
Digital reference applications now use Lerna 8.2.3. This release also updates the Lerna configuration format. This change is optional, however, it's recommended that you make the updates in this release.
- Create a new property in your package.json file called
"workspaces": []. - Move the list of packages in your lerna.json file to
workspacesin your package.json file. - In your
lerna.jsonfile, add the following properties:"$schema": "node_modules/lerna/schemas/lerna-schema.json","npmClient": "npm","useWorkspaces": true
- Remove any deprecated Lerna commands and options from your scripts and package.json file.
New file name utility
InsuranceSuite application affected: PolicyCenter
Digital reference application affected: ProducerEngage
A new file, FileNameMismatchException.gs has been added. If the file is not present in your base configuration, create a new file at gsrc/edge/security/fileupload/exception/FileNameMismatchException.gs and add the following content:
package edge.security.fileupload.exception
uses edge.exception.ApplicationErrorCode
uses edge.exception.ApplicationException
class FileNameMismatchException extends ApplicationException {
construct() {
super(ApplicationErrorCode.GW_SECURITY_ERROR)
}
construct(aMessage : String) {
super(ApplicationErrorCode.GW_SECURITY_ERROR)
Message = aMessage;
}
}Response header configuration changes
InsuranceSuite application affected: PolicyCenter, ClaimCenter
Digital reference application affected: CustomerEngage Account Management, CustomerEngage Account Management for ClaimCenter, ProducerEngage, ProducerEngage for ClaimCenter, ServiceRepEngage
- gsrc/edge/capabilities/gateway/document/DocumentUploadHandler.gs
- gsrc/edge/capabilities/policy/document/PolicyDocumentUploadHandler.gs
- gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs
- gsrc/edge/security/fileupload/FileUploadUtil.gs
- gsrc/edge/capabilities/claim/document/ClaimDocumentUploadHandler.gs
Changes to the document upload handler
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in DocumentUploadHandler.gs:
package edge.capabilities.gateway.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.security.fileupload.IFileUploadPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
class DocumentUploadHandler extends AbstractRpcHandler {
var _documentPlugin : IDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("fileUploadPlugin", "Plugin used to verify file upload validity")
construct(aDocumentPlugin : IDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
fileUploadPlugin : IFileUploadPlugin){
this._documentPlugin = aDocumentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Uploads a document
*
* <dl>
* <dt>Calls:</dt>
* <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
* <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
* <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
* <dt>Throws:</dt>
* <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
* <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
* <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
* </dl>
* @param documentDTO
* @param fileItem
* @return document metadata response
*/
@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
"at [0] is the document metadata; at [1] is the item to be uploaded")
@ApidocAvailableSince("5.0")
function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {
try {
// validate upload token is still valid
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException){
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
/** Some browsers (or fileupload library on some browsers) do not populate this field). */
if (documentDto.MimeType == null) {
documentDto.MimeType = documentFile.ContentType
}
final var doc = Bundle.resolveInTransaction( \ bundle -> {
try {
final var _doc = _documentPlugin.createDocument(documentDto)
// add the document to the dms
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, _doc)
return _doc
} finally {
documentFile.InputStream.close()
}
})
return _documentPlugin.toDTO(doc)
}
}package edge.capabilities.gateway.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.security.fileupload.DefaultFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil
class DocumentUploadHandler extends AbstractRpcHandler {
private static final var LOG = Logger.forCategory(DocumentUploadHandler.Type.QName)
private static final var TIKA = new Tika()
var _documentPlugin : IDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
@InjectableNode
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
construct(aDocumentPlugin : IDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin){
this._documentPlugin = aDocumentPlugin
this._documentSessionPlugin = documentSessionPlugin
}
/**
* Uploads a document
*
* <dl>
* <dt>Calls:</dt>
* <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
* <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
* <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
* <dt>Throws:</dt>
* <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
* <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
* <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
* </dl>
* @param documentDTO
* @param fileItem
* @return document metadata response
*/
@JsonRpcRunAsInternalGWUser
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
"at [0] is the document metadata; at [1] is the item to be uploaded")
@ApidocAvailableSince("5.0")
function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {
try {
// validate upload token is still valid
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException){
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if (documentDto.Name != documentFile.Name) {
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
if (documentDto.MimeType == null) {
documentDto.MimeType = documentFile.ContentType
}
FileUploadUtil.enforceMaxUploadSize(documentFile)
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
// Pass filename so we can disambiguate OOXML when Tika reports application/zip.
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
if (!isAllowedDetectedType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
}
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
documentDto.MimeType = detectedType
final var doc = Bundle.resolveInTransaction(\ bundle -> {
final var _doc = _documentPlugin.createDocument(documentDto)
var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
try {
Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, _doc)
} finally {
uploadInputStream.close()
}
return _doc
})
return _documentPlugin.toDTO(doc)
}
private static function isAllowedDetectedType(detectedType : String) : boolean {
return new DefaultFileUploadPlugin().canUploadContentType(detectedType)
}
}Changes to the policy document upload handler
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in PolicyDocumentUploadHandler.gs:
package edge.capabilities.policy.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
private var _policyAuthCheck : IPolicyAccessPlugin
private var _documentPlugin : IPolicyDocumentsPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("policyAuthCheck", "Policy access policy")
@Param("documentPlugin", "Plugin used to access documents associated with the policy")
@Param("documentSessionPlugin", "Document session management plugin")
construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
documentSessionPlugin : IDocumentSessionPlugin, fileUploadPlugin : IFileUploadPlugin) {
this._policyAuthCheck = policyAuthCheck
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._fileUploadPlugin = fileUploadPlugin
}
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. " +
"NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
@ApidocAvailableSince("5.0")
function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
try {
try {
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)
if (policyPeriod == null) {
throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
}
if (!_policyAuthCheck.hasAccess(policyPeriod)) {
throw new NoAuthorityException()
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
policyPeriod = bundle.add(policyPeriod)
final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
return doc
})
/* Should do this outside the transactions as public ID is not set inside the bundle. */
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}package edge.capabilities.policy.document
uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses edge.security.fileupload.FileUploadUtil
uses edge.security.fileupload.DefaultFileUploadPlugin
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
private var _policyAuthCheck : IPolicyAccessPlugin
private var _documentPlugin : IPolicyDocumentsPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private static final var TIKA = new Tika()
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
@InjectableNode
@Param("policyAuthCheck", "Policy access policy")
@Param("documentPlugin", "Plugin used to access documents associated with the policy")
@Param("documentSessionPlugin", "Document session management plugin")
construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
documentSessionPlugin : IDocumentSessionPlugin) {
this._policyAuthCheck = policyAuthCheck
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
}
@JsonRpcMethod
@ApidocMethodDescription("Uploads a document. " +
"NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
@ApidocAvailableSince("5.0")
function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
try {
try {
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name != documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
// Enforce max upload size
FileUploadUtil.enforceMaxUploadSize(documentFile)
// Read file bytes with limit
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
// Detect and normalize MIME type
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
// Enforce PDF name/content consistency
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
// Cross-check filename extension against bytes-detected MIME
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
// Centralized allowed content type check
if (!new DefaultFileUploadPlugin().canUploadContentType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
}
// Validate PDF/image content
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
// Persist the validated MIME on the DTO so stored metadata matches
documentDto.MimeType = detectedType
var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)
if (policyPeriod == null) {
throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
}
if (!_policyAuthCheck.hasAccess(policyPeriod)) {
throw new NoAuthorityException()
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
policyPeriod = bundle.add(policyPeriod)
final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
Plugins.get(IDocumentContentSource).addDocument(new java.io.ByteArrayInputStream(bytes), doc)
return doc
})
/* Should do this outside the transactions as public ID is not set inside the bundle. */
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}Changes to the service request file upload handler
Make these changes in gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs:
package edge.capabilities.servicerequest.document
uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
class ServiceRequestFileUploadHandler extends AbstractRpcHandler {
private var _documentPlugin : IServiceRequestDocumentPlugin
private var _retrievalPlugin : IServiceRequestRetrievalPlugin
private var _fileUploadPlugin: IFileUploadPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _authz : Authorizer<Document>
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("retrievalPlugin", "Plugin used to access service requests")
@Param("fileUploadPugin", "Plugin used to validate file validity")
public construct(documentPlugin : IServiceRequestDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
docAuthz:Authorizer<Document>,
retrievalPlugin : IServiceRequestRetrievalPlugin,
fileUploadPlugin: IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._retrievalPlugin = retrievalPlugin
this._authz = docAuthz
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Uploads a document
*
* Given a list of file items, it expects that the element
* <ul>
* <li> at [0] is the document metadata </li>
* <li> at [1] is the item to be uploaded </li>
* </ul>
*
* </br>
* Throws -
* <ul>
* <li>IllegalArgumentException If id number associated with the document is null or empty</li>
* <li>EntityNotFoundException If no service request statement is found</li>
* <li>AuthorizationException If the portal user has no access to the service request statement</li>
* </ul>
*
* @param list of file items
* @return document metadata response
*/
@JsonRpcMethod
function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)
if (serviceRequest == null) {
throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
serviceRequest = bundle.add(serviceRequest)
var docContent = new DocumentContent(){
:Content = documentFile.get(),
:MimeType = documentDto.MimeType
};
final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
if ( !_authz.canAccess(doc) ) {
throw new NoAuthorityException()
}
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
return doc
})
return _documentPlugin.toDTO(res)
} finally {
documentFile.InputStream.close()
}
}
}package edge.capabilities.servicerequest.document
uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.FileUploadUtil
uses org.apache.tika.Tika
class ServiceRequestFileUploadHandler extends AbstractRpcHandler {
private var _documentPlugin : IServiceRequestDocumentPlugin
private var _retrievalPlugin : IServiceRequestRetrievalPlugin
private var _fileUploadPlugin: IFileUploadPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _authz : Authorizer<Document>
private static final var TIKA = new Tika()
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("retrievalPlugin", "Plugin used to access service requests")
@Param("fileUploadPugin", "Plugin used to validate file validity")
public construct(documentPlugin : IServiceRequestDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
docAuthz:Authorizer<Document>,
retrievalPlugin : IServiceRequestRetrievalPlugin,
fileUploadPlugin: IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._retrievalPlugin = retrievalPlugin
this._authz = docAuthz
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Uploads a document
*
* Given a list of file items, it expects that the element
* <ul>
* <li> at [0] is the document metadata </li>
* <li> at [1] is the item to be uploaded </li>
* </ul>
*
* </br>
* Throws -
* <ul>
* <li>IllegalArgumentException If id number associated with the document is null or empty</li>
* <li>EntityNotFoundException If no service request statement is found</li>
* <li>AuthorizationException If the portal user has no access to the service request statement</li>
* </ul>
*
* @param list of file items
* @return document metadata response
*/
@JsonRpcMethod
function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch (ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
// Reject names with control / bidi / non-printable chars
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
FileUploadUtil.enforceMaxUploadSize(documentFile)
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + detectedType)
}
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
documentDto.MimeType = detectedType
var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)
if (serviceRequest == null) {
throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
serviceRequest = bundle.add(serviceRequest)
var docContent = new DocumentContent(){
:Content = bytes,
:MimeType = detectedType
};
final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
if ( !_authz.canAccess(doc) ) {
throw new NoAuthorityException()
}
var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
try {
Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
} finally {
uploadInputStream.close()
}
return doc
})
return _documentPlugin.toDTO(res)
} finally {
documentFile.InputStream.close()
}
}
}New file upload utility
A new FileUploadUtil.gs file has been added:
package edge.security.fileupload
uses java.io.ByteArrayInputStream
uses javax.imageio.ImageIO
uses java.io.IOException
uses javax.imageio.IIOException
uses java.nio.charset.StandardCharsets
uses java.net.URLEncoder
uses java.util.regex.Pattern
uses edge.security.fileupload.exception.IllegalContentTypeException
uses gw.api.util.Logger
uses org.apache.commons.fileupload2.core.FileItem
uses java.io.ByteArrayOutputStream
uses org.apache.tika.Tika
public class FileUploadUtil {
private static final var MAX_UPLOAD_BYTES = 25 * 1024 * 1024 // 25 MB
private static final var LOG = Logger.forCategory(FileUploadUtil.Type.QName)
private static final var TIKA = new Tika()
// Unicode control / bidi / zero-width / non-character ranges that break
// Content-Disposition serialization and enable RTL-override filename spoofing.
// Disallowed at upload; stripped at download for any pre-existing stored names.
// U+061C Arabic Letter Mark (ALM, bidi formatter)
// U+200B-200F zero-width and bidi marks (ZWSP, ZWNJ, ZWJ, LRM, RLM)
// U+202A-202E bidi embedding/override (incl. RLO U+202E)
// U+2066-2069 bidi isolates (LRI, RLI, FSI, PDI -- Trojan Source vectors)
// U+2028-2029 line/paragraph separators
// U+FEFF BOM / zero-width no-break space
// U+FFFD replacement character
// Plus all C0 (0x00-0x1F) and C1 (0x7F-0x9F) control chars.
private static final var DISALLOWED_NAME_CHAR : String =
"[\\u0000-\\u001F\\u007F-\\u009F\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2066-\\u2069\\u2028\\u2029\\uFEFF\\uFFFD]"
private static final var DISALLOWED_NAME_PATTERN : Pattern = Pattern.compile(DISALLOWED_NAME_CHAR)
// XENG-2487: characters that enable Content-Disposition parameter injection
// (`;`), quoted-string breakouts (`"`, `'`), or HTML metachars (`<`, `>`).
// The download-path header builder already escapes these; reject at upload
// as defense in depth.
private static final var HEADER_UNSAFE_CHAR : String = "[\";'<>]"
private static final var HEADER_UNSAFE_PATTERN : Pattern = Pattern.compile(HEADER_UNSAFE_CHAR)
private static final var DEFAULT_DOWNLOAD_FILENAME = "document"
public static function getMaxUploadBytes() : int {
return MAX_UPLOAD_BYTES
}
public static function enforceMaxUploadSize(documentFile : FileItem) {
var size : long
try {
size = documentFile.Size
} catch (e : Exception) {
// Fail closed: a missing/unreadable size indicates a malformed upload that
// we should reject rather than wave through with an unbounded read.
LOG.warn("Rejected upload: could not determine file size: " + e)
throw new IllegalContentTypeException("Uploaded file size could not be determined.")
}
if (size > MAX_UPLOAD_BYTES) {
throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
}
}
public static function readAllBytesAndCloseWithLimit(documentFile : FileItem, maxBytes : int) : byte[] {
var bos = new ByteArrayOutputStream()
var is = documentFile.InputStream
var buffer = new byte[8192]
var total = 0
try {
while (true) {
var read = is.read(buffer)
if (read < 0) {
break
}
total += read
if (total > maxBytes) {
throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
}
bos.write(buffer, 0, read)
}
var bytes = bos.toByteArray()
if (bytes.length == 0) {
throw new IllegalContentTypeException("Uploaded file is empty.")
}
return bytes
} finally {
try {
is.close()
} finally {
bos.close()
}
}
}
public static function normalizeMimeType(mime : String, fileName : String) : String {
var m = mime?.trim()?.toLowerCase()
// Both application/zip and application/x-tika-ooxml are opaque OOXML containers
// at this detection depth. Use the file extension to identify the specific type.
if (m == "application/zip" or m == "application/x-tika-ooxml") {
var mapped = mapZipByExtension(fileName)
if (mapped != null) {
return mapped
}
// No recognized extension — fall through to standard normalization.
// canUploadContentType will reject the unknown type.
}
// application/x-tika-msoffice is Tika's generic supertype for legacy OLE2 binary
// Office formats (.doc, .xls, .ppt, .msg). Palisades upgraded Tika returns this
// instead of the specific canonical MIME types that pre-date OOXML. Use the file
// extension to map back to the canonical type so the allowlist check can match.
if (m == "application/x-tika-msoffice") {
var mapped = mapOleByExtension(fileName)
if (mapped != null) {
return mapped
}
// No recognized extension — fall through; canUploadContentType will reject it.
}
// Tika sniffs CSV bytes as text/plain (the content is plain text), but the
// file extension implies text/csv. Normalize so assertExtensionMatchesType
// and the allowlist check both pass.
if (m == "text/plain") {
var n = fileName?.trim()?.toLowerCase()
if (n != null and n.endsWith(".csv")) {
return "text/csv"
}
}
// video/quicktime is Tika's catch-all for the ISO Base Media File Format (ISOBMFF)
// container family. MP4 files whose ftyp major brand is not one of the specific
// brands Tika has magic bytes for (mp41, mp42) are detected as video/quicktime even
// though they carry a .mp4 extension. Use the file extension to resolve to the
// correct specific type, mirroring the zip→OOXML and msoffice→OLE patterns above.
if (m == "video/quicktime") {
var mapped = mapQuicktimeByExtension(fileName)
if (mapped != null) {
return mapped
}
// .mov / .qt / unrecognised extension — stays video/quicktime.
}
return normalizeMimeType(mime)
}
public static function normalizeMimeType(mime : String) : String {
if (mime == null) {
return null
}
var m = mime.trim().toLowerCase()
if (m == "image/jpg") {
return "image/jpeg"
}
if (m == "application/x-pdf") {
return "application/pdf"
}
// RFC 4337 defines application/mp4 as the generic MP4 container type.
// Tika may return this from extension-based detection while returning video/mp4
// from bytes-based detection (or vice versa). Normalize to video/mp4 so the
// assertExtensionMatchesType comparison and the allowlist check both succeed.
if (m == "application/mp4") {
return "video/mp4"
}
// Tika 3.x canonical type for WAV is audio/vnd.wave; audio/x-wav and audio/wave
// are registered aliases. The allowlist entry is audio/wav (the IANA short form).
// Normalize all three aliases so the allowlist check and extension comparison succeed.
if (m == "audio/vnd.wave" or m == "audio/x-wav" or m == "audio/wave") {
return "audio/wav"
}
// image/x-bmp and image/x-ms-bmp are legacy aliases for image/bmp (IANA-registered
// since RFC 7903). Tika 3.x returns the canonical image/bmp but normalize the aliases
// as defence-in-depth in case a client or older detector path returns one.
if (m == "image/x-bmp" or m == "image/x-ms-bmp") {
return "image/bmp"
}
// application/x-tika-ooxml and application/x-tika-msoffice are intentionally NOT
// resolved here — a filename is required. Use normalizeMimeType(mime, fileName).
return m
}
private static function mapZipByExtension(fileName : String) : String {
if (fileName == null) {
return null
}
var n = fileName.trim().toLowerCase()
if (n.endsWith(".docx")) {
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
if (n.endsWith(".xlsx")) {
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}
if (n.endsWith(".pptx")) {
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
}
return null
}
private static function mapOleByExtension(fileName : String) : String {
if (fileName == null) {
return null
}
var n = fileName.trim().toLowerCase()
if (n.endsWith(".doc") or n.endsWith(".dot")) {
return "application/msword"
}
if (n.endsWith(".xls") or n.endsWith(".xlt") or n.endsWith(".xlm")) {
return "application/vnd.ms-excel"
}
if (n.endsWith(".ppt") or n.endsWith(".pot") or n.endsWith(".pps")) {
return "application/vnd.ms-powerpoint"
}
if (n.endsWith(".msg")) {
return "application/vnd.ms-outlook"
}
return null
}
// Tika detects the QuickTime/ISOBMFF container family as video/quicktime when the
// file's ftyp major brand does not match one of the more specific magic-byte patterns
// (e.g. ftypmp41/mp42 for video/mp4). Use the file extension to resolve to the
// correct allowlisted subtype. .mov and .qt stay as video/quicktime (their own glob
// entries in Tika); anything unrecognised returns null so the caller falls through.
private static function mapQuicktimeByExtension(fileName : String) : String {
if (fileName == null) {
return null
}
var n = fileName.trim().toLowerCase()
if (n.endsWith(".mp4") or n.endsWith(".mp4v") or n.endsWith(".mpg4")) {
return "video/mp4"
}
// .mov and .qt are the native QuickTime extensions — keep video/quicktime.
if (n.endsWith(".mov") or n.endsWith(".qt")) {
return "video/quicktime"
}
return null
}
public static function isDecodableImage(bytes : byte[]) : boolean {
try {
return ImageIO.read(new ByteArrayInputStream(bytes)) != null
} catch (e : IIOException) {
LOG.debug("Image decode failed")
return false
} catch (e : IOException) {
LOG.debug("Image decode failed")
return false
} catch (e : RuntimeException) {
LOG.debug("Image decode failed")
return false
}
}
public static function passesPdfSanityCheck(bytes : byte[]) : boolean {
if (bytes == null or bytes.length < 10) {
return false
}
var headerLen = Math.min(8, bytes.length)
var header = new String(bytes, 0, headerLen, StandardCharsets.ISO_8859_1)
if (!header.startsWith("%PDF-")) {
return false
}
var tailWindow = Math.min(64 * 1024, bytes.length)
var tail = new String(bytes, bytes.length - tailWindow, tailWindow, StandardCharsets.ISO_8859_1)
var eofIndex = tail.lastIndexOf("%%EOF")
if (eofIndex < 0) {
return false
}
var startxrefIndex = tail.lastIndexOf("startxref")
if (startxrefIndex < 0 or startxrefIndex > eofIndex) {
return false
}
var hasClassicXref = tail.contains("\nxref") or tail.contains("\rxref")
var hasClassicTrailer = tail.contains("trailer")
var hasXrefStreamMarker = tail.contains("/XRef")
return (hasClassicXref and hasClassicTrailer) or hasXrefStreamMarker
}
public static function enforcePdfNameConsistency(fileName : String, detectedType : String, mimePdf : String) {
if (fileName == null) {
return
}
var n = fileName.trim().toLowerCase()
var looksLikePdf = n.endsWith(".pdf") or n.contains(".pdf.")
if (looksLikePdf and detectedType != mimePdf) {
LOG.info("Rejected upload: filename indicates PDF but detected content is not PDF")
throw new IllegalContentTypeException("File name indicates a PDF, but the uploaded file is not a valid PDF.")
}
}
public static function validateContent(detectedType : String, bytes : byte[], mimePdf : String, mimePng : String, mimeJpeg : String) {
switch (detectedType) {
case mimePdf:
if (!passesPdfSanityCheck(bytes)) {
LOG.info("Rejected PDF upload: invalid content")
throw new IllegalContentTypeException("The uploaded PDF appears to be invalid or corrupted. Please re-save and try again.")
}
break
case mimePng:
case mimeJpeg:
if (!isDecodableImage(bytes)) {
LOG.info("Rejected image upload: invalid content")
throw new IllegalContentTypeException("The uploaded image appears to be invalid or corrupted. Please re-save and try again.")
}
break
default:
break
}
}
// Reject file names containing Unicode control / bidi / zero-width / non-printable chars.
// These characters break Content-Disposition serialization (causing browsers to render
// attachments inline) and enable RTL-override extension spoofing.
public static function assertSafeFileName(fileName : String) {
if (fileName == null or fileName.length == 0) {
throw new IllegalContentTypeException("File name is required.")
}
var matcher = DISALLOWED_NAME_PATTERN.matcher(fileName)
if (matcher.find()) {
// Report the first offending char as U+XXXX. Never echo the raw character
// into the log or exception message — control/bidi chars can break log
// parsers and enable log-injection.
var codePoint = formatCodePoint(fileName.charAt(matcher.start()) as int)
LOG.info("Rejected upload: file name contains disallowed character " + codePoint)
throw new IllegalContentTypeException(
"File name contains disallowed control or formatting character (" + codePoint + ").")
}
// XENG-2487: reject header-injection / quote-breakout / HTML metachars.
var injMatcher = HEADER_UNSAFE_PATTERN.matcher(fileName)
if (injMatcher.find()) {
var ch = fileName.charAt(injMatcher.start())
LOG.info("Rejected upload: file name contains disallowed character '" + ch + "'")
throw new IllegalContentTypeException("File name must not contain '" + ch + "'.")
}
// Reject path separators so a malicious name cannot influence storage paths.
if (fileName.contains("/") or fileName.contains("\\")) {
LOG.info("Rejected upload: file name contains path separator")
throw new IllegalContentTypeException("File name must not contain path separators.")
}
}
private static function formatCodePoint(code : int) : String {
var hex = Integer.toHexString(code).toUpperCase()
var sb = new StringBuilder("U+")
for (i in 0..|(4 - hex.length)) {
sb.append("0")
}
sb.append(hex)
return sb.toString()
}
// Strip disallowed characters. Used as defense-in-depth on the download path so any
// names stored before the upload-side fix do not break the Content-Disposition header.
public static function sanitizeFileName(fileName : String) : String {
if (fileName == null) {
return DEFAULT_DOWNLOAD_FILENAME
}
var stripped = DISALLOWED_NAME_PATTERN.matcher(fileName).replaceAll("")
.replaceAll("[\\\\/]", "_")
.trim()
return stripped.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : stripped
}
// Build an RFC 6266 Content-Disposition header value: an ASCII-only quoted fallback
// plus a UTF-8 percent-encoded `filename*` for compliant clients. Always returns
// `attachment; ...` so the browser never renders the document inline.
public static function buildContentDisposition(fileName : String) : String {
var safeName = sanitizeFileName(fileName)
var asciiFallback = toAsciiFallback(safeName)
var utf8Encoded = percentEncodeUtf8(safeName)
return "attachment; filename=\"" + asciiFallback + "\"; filename*=UTF-8''" + utf8Encoded
}
// Replace anything outside printable ASCII (0x20-0x7E) with "_" and backslash-escape
// any embedded quote or backslash so the value is safe inside the quoted-string per RFC 6266.
private static function toAsciiFallback(name : String) : String {
var sb = new StringBuilder()
for (i in 0..|name.length) {
var code = (name.charAt(i) as int)
if (code < 0x20 or code > 0x7E) {
sb.append("_")
} else if (code == 0x22 or code == 0x5C) { // " or \
sb.append("\\").append(name.charAt(i))
} else {
sb.append(name.charAt(i))
}
}
var result = sb.toString()
return result.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : result
}
private static function percentEncodeUtf8(name : String) : String {
try {
// URLEncoder follows application/x-www-form-urlencoded rules, which differ
// from RFC 5987 attr-char in two places: spaces become "+" (we convert to
// %20) and "*" is left literal (we percent-encode to %2A so the filename*
// value stays RFC-compliant for filenames like "budget*final.pdf").
return URLEncoder.encode(name, StandardCharsets.UTF_8.name())
.replace("+", "%20")
.replace("*", "%2A")
} catch (e : java.io.UnsupportedEncodingException) {
LOG.debug("UTF-8 encoding unavailable when building Content-Disposition: " + e)
return DEFAULT_DOWNLOAD_FILENAME
}
}
// Cross-check the filename's implied content type (from extension via Tika)
// against the bytes-detected type. Catches the `evil.exe`-with-image-bytes
// pattern where bytes pass the allow-list but the filename extension would
// not. Skips the check when the extension is unknown to Tika so files like
// README (no extension) still go through.
public static function assertExtensionMatchesType(fileName : String, detectedType : String) {
if (fileName == null) {
return
}
var extType : String
try {
extType = TIKA.detect(fileName)
} catch (e : Exception) {
LOG.debug("Could not infer type from filename '" + fileName + "': " + e)
return
}
if (extType == null or extType == "application/octet-stream") {
return
}
var normalizedExt = normalizeMimeType(extType, fileName)
if (normalizedExt != detectedType) {
LOG.info("Rejected upload: filename extension implies " + normalizedExt
+ " but content sniffs as " + detectedType)
throw new IllegalContentTypeException(
"File extension does not match detected content type.")
}
}
}Changes to the claim document upload handler
The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in ClaimDocumentUploadHandler.gs:
package edge.capabilities.claim.document
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.capabilities.claim.document.IClaimDocumentPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class ClaimDocumentUploadHandler extends AbstractRpcHandler {
private var _documentPlugin : IClaimDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _claimRetrievalPlugin : IClaimRetrievalPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("claimRetrievalPlugin", "Plugin used to access claim data")
@Param("fileUploadPlugin", "Plugin used to verify file upload validity")
construct(documentPlugin : IClaimDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
claimRetrievalPlugin : IClaimRetrievalPlugin,
fileUploadPlugin : IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._claimRetrievalPlugin = claimRetrievalPlugin
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Used to upload claim documents from the frontend.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
* <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
* <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
* <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
* <dt>Throws:</dt>
* <dd><code>AuthorizationException</code> - If session is invalid</dd>
* <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
* <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
* </dl>
*
* @param documentDto a DTO describing the metadata for the document
* @param documentFile the document itself
* @returns A claimDocumentDTO for display in the frontend
* */
@JsonRpcMethod
@ApidocMethodDescription("Used to upload claim documents from the frontend.")
@ApidocAvailableSince("5.0")
function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch ( ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
if(documentDto.MimeType!=documentFile.ContentType) {
throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
}
if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
}
var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)
if (claim == null) {
throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
claim = bundle.add(claim)
final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
return doc
})
/* Should do this outside the transactions as public ID is not set inside the bundle. */
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}package edge.capabilities.claim.document
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil
/**
* Handler of document uploads.
* It is needed to work around "security through obscurity" in the authz service and its inability
* to cope with many different access modes for the same URL. It is also a workaround for a third-party component
* which is also inflexible and could not accommodate different transports.
*/
class ClaimDocumentUploadHandler extends AbstractRpcHandler {
private static final var LOG = Logger.forCategory(ClaimDocumentUploadHandler.Type.QName)
private static final var TIKA = new Tika()
static final var MIME_PNG = "image/png"
static final var MIME_JPEG = "image/jpeg"
static final var MIME_PDF = "application/pdf"
private var _documentPlugin : IClaimDocumentPlugin
private var _documentSessionPlugin : IDocumentSessionPlugin
private var _claimRetrievalPlugin : IClaimRetrievalPlugin
private var _fileUploadPlugin : IFileUploadPlugin
@InjectableNode
@Param("documentPlugin", "Plugin used to deal with claim documents")
@Param("documentSessionPlugin", "Plugin used to deal with document sessions")
@Param("claimRetrievalPlugin", "Plugin used to access claim data")
@Param("fileUploadPlugin", "Plugin used to verify file upload validity")
construct(documentPlugin : IClaimDocumentPlugin,
documentSessionPlugin : IDocumentSessionPlugin,
claimRetrievalPlugin : IClaimRetrievalPlugin,
fileUploadPlugin : IFileUploadPlugin) {
this._documentPlugin = documentPlugin
this._documentSessionPlugin = documentSessionPlugin
this._claimRetrievalPlugin = claimRetrievalPlugin
this._fileUploadPlugin = fileUploadPlugin
}
/**
* Used to upload claim documents from the frontend.
*
* <dl>
* <dt>Calls:</dt>
* <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
* <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
* <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
* <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
* <dt>Throws:</dt>
* <dd><code>AuthorizationException</code> - If session is invalid</dd>
* <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
* <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
* </dl>
*
* @param documentDto a DTO describing the metadata for the document
* @param documentFile the document itself
* @returns A claimDocumentDTO for display in the frontend
* */
@JsonRpcMethod
@ApidocMethodDescription("Used to upload claim documents from the frontend.")
@ApidocAvailableSince("5.0")
function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
try {
try {
// validate document upload token
_documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
} catch ( ex : JsonRpcSecurityException) {
throw new AuthorizationException(){:Message = "Unauthorized portal access"}
}
if(documentDto.Name!=documentFile.Name){
throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
}
FileUploadUtil.assertSafeFileName(documentDto.Name)
FileUploadUtil.assertSafeFileName(documentFile.Name)
FileUploadUtil.enforceMaxUploadSize(documentFile)
var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)
if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
}
FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
documentDto.MimeType = detectedType
var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)
if (claim == null) {
throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
}
final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
claim = bundle.add(claim)
final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
try {
Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
} finally {
uploadInputStream.close()
}
return doc
})
return _documentPlugin.getDocumentDetails(res)
} finally {
documentFile.InputStream.close()
}
}
}