diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7f7f567..dff150a8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #822: The CPF resource processor now supports system expressions and macros in CPF merge files
- #578 Added functionality to record and display IPM history of install, uninstall, load, and update
- #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install.
-
+- #973: Enables CORS and JWT configuration for WebApplications in module xml
### Changed
- #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies
- #885: Always synchronously load dependencies and let each module do multi-threading as needed
diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls
index 8734ec9a..540d23b0 100644
--- a/src/cls/IPM/Main.cls
+++ b/src/cls/IPM/Main.cls
@@ -1329,6 +1329,7 @@ ClassMethod GenerateModuleXML(ByRef pCommandInfo) As %Status [ Internal ]
do ##class(%Library.Prompt).GetString("Enter module keywords:", .tKeywords)
+ #dim tTemplate As %IPM.Storage.ModuleTemplate
set tTemplate = ##class(%IPM.Storage.ModuleTemplate).NewTemplate(tPath, tName, tVersion, tDescription, tKeywords)
return:'$isobject(tTemplate)
@@ -1361,6 +1362,44 @@ ClassMethod GenerateModuleXML(ByRef pCommandInfo) As %Status [ Internal ]
}
do ##class(%Library.Prompt).GetString(" Enter a comma separated list of web applications or * for all:", .tWebAppList)
+#if $$$CacheVersionMajor>=2025
+ //cors
+ write !,"Cross-Origin Settings:"
+ do ##class(%Library.Prompt).GetYesNo("Configure Access-Control-Allow-Credentials:",.enableCors)
+ if enableCors {
+ do ##class(%Library.Prompt).GetString(" Enter a comma separated list of Allowed Headers:",.allowedHeaders,,32767,"comma seperated values e.g: Access-Control-Allow-Origin,Access-Control-Allow-Headers")
+ while (1) {
+ do ##class(%Library.Prompt).GetString(" Enter a comma separated list of Allowed Origins:",.allowedOrigins,,32767,"comma seperated values e.g: http://www.example.com")
+ if allowedOrigins="" quit
+ set sc = tTemplate.ValidateCorsOrigin(allowedOrigins)
+ if $$$ISERR(sc) {
+ do ..DisplayError(sc)
+ set allowedOrigins = ""
+ continue
+ }
+ quit
+ }
+ set corsConfig("enableCors") = enableCors
+ set corsConfig("allowedHeaders") = allowedHeaders
+ set corsConfig("allowedOrigins") = allowedOrigins
+ do tTemplate.SetCORSProps(tWebAppList,.corsConfig)
+ }
+#endif
+
+#if $$$CacheVersionMajor>=2024
+ //jwt
+ write !,"JWT Authentication"
+ do ##class(%Library.Prompt).GetYesNo("Configure JWT Authentication: ",.enableJWT)
+ if enableJWT {
+ do ##class(%Library.Prompt).GetNumber(" Enter JWT Access Token Timeout:",.JWTAccessTokenTO,1)
+ do ##class(%Library.Prompt).GetNumber(" Enter JWT Refresh Token Timeout:",.JWTRefreshTokenTO,1)
+ set jwtconfig("enableJWT") = enableJWT
+ set jwtconfig("JWTAccessTokenTO") = JWTAccessTokenTO
+ set jwtconfig("JWTRefreshTokenTO") = JWTRefreshTokenTO
+ do tTemplate.SetJWTProps(tWebAppList,.jwtconfig)
+ }
+#endif
+
do tTemplate.AddWebApps(tWebAppList,.tCSPapps) // tCSP - list of CSP (not REST apps)
for i=1:1:$listlength(tCSPapps) {
set tCSPPath = ""
diff --git a/src/cls/IPM/Storage/ModuleTemplate.cls b/src/cls/IPM/Storage/ModuleTemplate.cls
index c2b948a4..fca2b5ff 100644
--- a/src/cls/IPM/Storage/ModuleTemplate.cls
+++ b/src/cls/IPM/Storage/ModuleTemplate.cls
@@ -185,7 +185,7 @@ Method AddWebApps(
{
set tAppList = ""
set pApps = $zstrip(pApps,"<>W")
- if ( pApps = "*" ) {
+ if (pApps = "*") {
do ..GetCSPApplications(.tAppList)
} else {
set tAppList = $listfromstring(pApps,",")
@@ -284,6 +284,24 @@ Method SetSourcePathForCSPApp(
set ..TemplateResources(pCSPApp,"Path") = pPath
}
+Method DefineReminWebAppProps(ByRef props)
+{
+ //add cors origin details
+ set props("CorsAllowlist") = ""
+ set props("CorsCredentialsAllowed") = 0
+ // add cors headers
+ set props("CorsHeadersList") = ""
+ set props("DeepSeeEnabled") = 0
+ set props("JWTAuthEnabled") = 0
+ set props("JWTRefreshTokenTimeout") = 900
+ set props("JWTAccessTokenTimeout") = 60
+ set props("WSGIAppLocation") = ""
+ set props("WSGIAppName") = ""
+ set props("WSGICallable") = ""
+ set props("WSGIDebug") = ""
+ set props("WSGIType") = ""
+}
+
ClassMethod GetGlobalsList(Output globals As %List) As %Status
{
set globals=""
@@ -352,6 +370,58 @@ Method SetAuthorProps(
return $$$OK
}
+Method SetCORSProps(
+ apps As %String = "",
+ ByRef cors)
+{
+ set appList = ""
+ set apps = $zstrip(apps,"<>W")
+ if (apps = "*") {
+ $$$ThrowOnError(..GetCSPApplications(.appList))
+ } else {
+ set appList = $listfromstring(apps,",")
+ }
+ set ptr = 0
+ while $listnext(appList, ptr, webpApp) {
+ set ..TemplateResources(webpApp,"CorsCredentialsAllowed") = cors("enableCors")
+ set ..TemplateResources(webpApp,"CorsHeadersList") = cors("allowedHeaders")
+ set ..TemplateResources(webpApp,"CorsAllowlist") = cors("allowedOrigins")
+ }
+}
+
+Method SetJWTProps(
+ apps As %String = "",
+ ByRef jwt)
+{
+ set appList = ""
+ set apps = $zstrip(apps,"<>W")
+ if (apps = "*") {
+ $$$ThrowOnError(..GetCSPApplications(.appList))
+ } else {
+ set appList = $listfromstring(apps,",")
+ }
+ set ptr = 0
+ while $listnext(appList, ptr, webpApp) {
+ set ..TemplateResources(webpApp,"JWTAccessTokenTimeout") = jwt("JWTAccessTokenTO")
+ set ..TemplateResources(webpApp,"JWTAuthEnabled") = jwt("allowedHeaders")
+ set ..TemplateResources(webpApp,"JWTRefreshTokenTimeout") = jwt("JWTRefreshTokenTO")
+ }
+}
+
+Method ValidateCorsOrigin(pAllowedOrigins As %String = "") As %Status
+{
+ new $namespace
+ set $namespace = "%SYS"
+ set originsList = $listfromstring(pAllowedOrigins,",")
+ set ptr = 0
+ set sc = $$$OK
+ while $listnext(originsList, ptr, origin){
+ set sc = ##class(Security.Applications).ValidateCorsOrigin(origin)
+ if $$$ISERR(sc) quit
+ }
+ return sc
+}
+
ClassMethod NewTemplate(
pPath,
pName,
@@ -525,6 +595,11 @@ Method SetTemplateProps() As %Status
set ..TemplateResources("rest","UnauthenticatedEnabled") = 0
set ..TemplateResources("rest","Recurse") = 1
+ //CORS
+ set ..TemplateResources("rest","CorsAllowlist") = "http://www.example.com"
+ set ..TemplateResources("rest","CorsCredentialsAllowed") = 1
+ set ..TemplateResources("rest","CorsHeadersList") = "Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Access-Control-Max-Age, Access-Control-Expose-Headers, Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
+
// WEB APP
set ..TemplateResources("web") = "/web"
set ..TemplateResources("web","Url") = "/web"
@@ -600,9 +675,9 @@ XData XSLT
-
+
-
+
diff --git a/tests/integration_tests/Test/PM/Integration/ResourceProcessor/WebApplication.cls b/tests/integration_tests/Test/PM/Integration/ResourceProcessor/WebApplication.cls
new file mode 100644
index 00000000..ae2a9afc
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/ResourceProcessor/WebApplication.cls
@@ -0,0 +1,72 @@
+/// This class validates that CORS headers and allowed origins are configured correctly,
+/// and that JWT authentication is properly set up in the <WebApplication> configuration section.
+Class Test.PM.Integration.ResourceProcessor.WebApplication Extends Test.PM.Integration.Base
+{
+
+Parameter CommonPathPrefix As STRING = "cors-rest-apps";
+
+Parameter WebAppName As STRING = "/testcors";
+
+/// Test.PM.Integration.Uninstall
+Method TestCORSEnabledWebAppViaModule()
+{
+ #define NormalizeDirectory(%path) ##class(%File).NormalizeDirectory(%path)
+ #define UTRoot ^UnitTestRoot
+
+ set testRoot = $$$NormalizeDirectory($get($$$UTRoot))
+ set moduleDir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/"_..#CommonPathPrefix_"/")
+ set moduleFile = ##class(%File).NormalizeFilename("module.xml",moduleDir)
+ if ##class(%File).DirectoryExists(moduleFile) {
+ do $$$AssertStatusOK(1,"module.xml File exist on "_moduleDir)
+ }
+ set status = ##class(%IPM.Main).Shell("load "_moduleDir)
+ do $$$AssertStatusOK(status,"Loaded "_..#CommonPathPrefix_" module successfully from "_moduleDir)
+ do ..VerifyCORSConfiguration()
+ do ..VerifyJWTConfiguration()
+
+ set status = ##class(%IPM.Main).Shell("uninstall "_..#CommonPathPrefix)
+ do $$$AssertStatusOK(status,"uninstalled "_..#CommonPathPrefix_" module successfully.")
+}
+
+Method VerifyCORSConfiguration()
+{
+ new $namespace
+ set $namespace = "%SYS"
+ set status = ##class(Security.Applications).Get(..#WebAppName, .props)
+ do $$$AssertStatusOK(status,"Web applciation "_..#WebAppName_" created scuccessfully")
+ if $data(props("CorsAllowlist"),corsAllowlist) {
+ do $$$AssertStatusOK(1,"CorsAllowlist values are defined")
+ do $$$LogMessage(corsAllowlist)
+ }
+ if $data(props("CorsCredentialsAllowed"),corsAllow) {
+ do $$$AssertStatusOK(1,"CorsCredentialsAllowed values are defined")
+ do $$$LogMessage(corsAllow)
+ }
+ if $data(props("CorsHeadersList"),corsHeadersList) {
+ do $$$AssertStatusOK(1,"CorsHeadersList values are defined")
+ do $$$LogMessage(corsHeadersList)
+ }
+}
+
+Method VerifyJWTConfiguration()
+{
+ new $namespace
+ set $namespace = "%SYS"
+ set status = ##class(Security.Applications).Get(..#WebAppName, .props)
+ do $$$AssertStatusOK(status,"Web applciation "_..#WebAppName_" created scuccessfully")
+ do $$$LogMessage("Validating JWT configuration")
+ if $data(props("JWTAccessTokenTimeout"),JWTAccessTokenTimeout) {
+ do $$$AssertStatusOK(1,"JWTAccessTokenTimeout value is defined")
+ do $$$LogMessage(JWTAccessTokenTimeout)
+ }
+ if $data(props("JWTAuthEnabled"),JWTAuthEnabled) {
+ do $$$AssertStatusOK(1,"JWTAuthEnabled value is defined")
+ do $$$LogMessage(JWTAuthEnabled)
+ }
+ if $data(props("JWTRefreshTokenTimeout"),JWTRefreshTokenTimeout) {
+ do $$$AssertStatusOK(1,"JWTRefreshTokenTimeout value is defined")
+ do $$$LogMessage(JWTRefreshTokenTimeout)
+ }
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/cors-rest-apps/module.xml b/tests/integration_tests/Test/PM/Integration/_data/cors-rest-apps/module.xml
new file mode 100644
index 00000000..bc003eef
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/cors-rest-apps/module.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ cors-rest-apps
+ 1.0.0
+ cors enabling testing on webapplication
+ cors
+ module
+
+
+ %IPM.Lifecycle.Module
+ src
+
+
+
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/cors-rest-apps/src/cls/CorsTest/Rest/Cors.cls b/tests/integration_tests/Test/PM/Integration/_data/cors-rest-apps/src/cls/CorsTest/Rest/Cors.cls
new file mode 100644
index 00000000..93d6dd75
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/cors-rest-apps/src/cls/CorsTest/Rest/Cors.cls
@@ -0,0 +1,17 @@
+Class CorsTest.Rest.Cors Extends %RegisteredObject
+{
+
+XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
+{
+
+
+
+}
+
+ClassMethod GetInfo() As %Status
+{
+ write "Hello, World!"
+ quit $$$OK
+}
+
+}