-
Notifications
You must be signed in to change notification settings - Fork 25
feat: Enables CORS and JWT configuration for WebApplications in CspApplication tag #973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
746fa55
e88d6f1
2897899
eb36e6b
f38df20
eb90948
e23691b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1272,6 +1272,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) | ||
|
|
||
|
|
@@ -1304,6 +1305,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 = "" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -275,6 +275,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="" | ||
|
|
@@ -343,6 +361,58 @@ Method SetAuthorProps( | |
| return $$$OK | ||
| } | ||
|
|
||
| Method SetCORSProps( | ||
| pApps As %String = "", | ||
|
||
| ByRef pCors As %String = "") | ||
| { | ||
| set appList = "" | ||
| set pApps = $zstrip(pApps,"<>W") | ||
| if ( pApps = "*" ) { | ||
|
||
| do ..GetCSPApplications(.appList) | ||
|
||
| } else { | ||
| set appList = $listfromstring(pApps,",") | ||
| } | ||
| set ptr = 0 | ||
| while $listnext(appList, ptr, webpApp){ | ||
| set ..TemplateResources(webpApp,"CorsCredentialsAllowed") = pCors("enableCors") | ||
| set ..TemplateResources(webpApp,"CorsHeadersList") = pCors("allowedHeaders") | ||
| set ..TemplateResources(webpApp,"CorsAllowlist") = pCors("allowedOrigins") | ||
| } | ||
| } | ||
|
|
||
| Method SetJWTProps( | ||
| pApps As %String = "", | ||
| ByRef pJWT As %String = "") | ||
| { | ||
| set appList = "" | ||
| set pApps = $zstrip(pApps,"<>W") | ||
| if ( pApps = "*" ) { | ||
| do ..GetCSPApplications(.appList) | ||
| } else { | ||
| set appList = $listfromstring(pApps,",") | ||
| } | ||
| set ptr = 0 | ||
| while $listnext(appList, ptr, webpApp){ | ||
| set ..TemplateResources(webpApp,"JWTAccessTokenTimeout") = pJWT("JWTAccessTokenTO") | ||
| set ..TemplateResources(webpApp,"JWTAuthEnabled") = pJWT("allowedHeaders") | ||
| set ..TemplateResources(webpApp,"JWTRefreshTokenTimeout") = pJWT("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){ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: missing space between
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed the space issue |
||
| set sc = ##class(Security.Applications).ValidateCorsOrigin(origin) | ||
| if $$$ISERR(sc) quit | ||
| } | ||
| return sc | ||
| } | ||
|
|
||
| ClassMethod NewTemplate( | ||
| pPath, | ||
| pName, | ||
|
|
@@ -515,6 +585,10 @@ Method SetTemplateProps() As %Status | |
| set ..TemplateResources("rest","PasswordAuthEnabled") = 1 | ||
| 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" | ||
|
|
@@ -591,9 +665,9 @@ XData XSLT | |
| </Export> | ||
| </xsl:template> | ||
| <xsl:template match="Resource[@Url]"> | ||
| <CSPApplication> | ||
| <WebApplication> | ||
| <xsl:apply-templates select="@*[local-name() != 'Name']" /> | ||
| </CSPApplication> | ||
| </WebApplication> | ||
| </xsl:template> | ||
| <xsl:template match="node()"> | ||
| <xsl:copy> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <b><WebApplication></b> configuration section. | ||
| Class Test.PM.Integration.ConfigCorsAndJWTInWebAppTest 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) | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <Export generator="Cache" version="25"> | ||
| <Document name="cors-rest-apps.ZPM"> | ||
| <Module> | ||
| <Name>cors-rest-apps</Name> | ||
| <Version>1.0.0</Version> | ||
| <Description>cors enabling testing on webapplication</Description> | ||
| <Keywords>cors</Keywords> | ||
| <Packaging>module</Packaging> | ||
| <Resource Name="CorsTest.PKG"/> | ||
| <WebApplication | ||
| CookiePath="/testcors" | ||
| CorsAllowlist="https://www.example.com,https://pm.intersystems.com" | ||
| CorsCredentialsAllowed="1" | ||
| JWTAccessTokenTimeout="60" | ||
| JWTAuthEnabled="1" | ||
| JWTRefreshTokenTimeout="900" | ||
| 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, Access-Control-Request-Method, Access-Control-Request-Headers" | ||
| PasswordAuthEnabled="1" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For WebApplication, these properties for auth are all encapsulated as part of AutheEnabled and not separate properties.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @isc-kiyer
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Presumably this refers to how AutheEnabled is a bit string with these properties, e.g. AutheEnabled=32 means PasswordAuthEnabled=1
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @isc-kiyer , @isc-dchui
<WebApplication
Url="/testcors"
CookiePath="/testcors"
AutheEnabled="32"
JWTAuthEnabled="1"
JWTAccessTokenTimeout="60"
JWTRefreshTokenTimeout="900"
CorsCredentialsAllowed="1"
CorsAllowlist="https://www.example.com"
CorsHeadersList="Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers"
PasswordAuthEnabled="1"
UnauthenticatedEnabled="0"
Recurse="1"
UseCookies="2" />
Could you check and suggest. |
||
| Recurse="1" | ||
| UnauthenticatedEnabled="0" | ||
| Url="/testcors" | ||
| UseCookies="2"/> | ||
| <LifecycleClass>%IPM.Lifecycle.Module</LifecycleClass> | ||
| <SourcesRoot>src</SourcesRoot> | ||
| </Module> | ||
| </Document> | ||
| </Export> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| Class CorsTest.Rest.Cors Extends %RegisteredObject | ||
| { | ||
|
|
||
| XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ] | ||
| { | ||
| <Routes> | ||
| <Route Url="/" Method="GET" Call="GetInfo" Cors="true"/> | ||
| </Routes> | ||
| } | ||
|
|
||
| ClassMethod GetInfo() As %Status | ||
| { | ||
| write "Hello, World!" | ||
| quit $$$OK | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| Class Test.PM.Unit.WebAppCorsTest Extends %UnitTest.TestCase | ||
| { | ||
|
|
||
| Parameter CommonPathPrefix As STRING = "cors-rest-apps"; | ||
|
|
||
| Parameter WebAppName As STRING = "/testcors"; | ||
|
|
||
| /// create web application using <WebApplcation> tag | ||
| Method TestCORSEnabledViaWebAppTag() | ||
| { | ||
| do $$$LogMessage("loading via <WebApplcation> tag") | ||
| set testRoot = ##class(%File).NormalizeDirectory($get(^UnitTestRoot)) | ||
| set moduleDir = ##class(%File).NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/"_..#CommonPathPrefix_"/webapp/") | ||
| if '##class(%File).DirectoryExists(moduleDir) { | ||
| do ##class(%File).CreateDirectoryChain(moduleDir) | ||
| } | ||
| set moduleFile = ##class(%File).NormalizeFilename("module.xml",moduleDir) | ||
| if ##class(%File).Exists(moduleFile){ | ||
| set status= ##class(%File).Delete(moduleFile) | ||
| do $$$AssertStatusOK(status,"Removed the 'module.xml' file from "_moduleDir) | ||
| } | ||
| do $$$LogMessage("Creating module.xml with <WebApplcation> tag") | ||
| set moduleStream = ##class(%Dictionary.XDataDefinition).%OpenId($classname()_"||ModuleWithWebApTag").Data | ||
| set fileStream = ##class(%Stream.FileBinary).%New() | ||
| set fileStream.Filename=moduleFile | ||
| set status= fileStream.CopyFromAndSave(moduleStream) | ||
| do $$$AssertStatusOK(status,"Created module.xml manually on "_moduleDir_" successfully.") | ||
|
|
||
| set status = ##class(%IPM.Main).Shell("load "_moduleDir) | ||
| do $$$AssertStatusOK(status,"Loaded "_..#CommonPathPrefix_" module successfully. "_moduleDir) | ||
| do ..VerifyCORSSettings() | ||
|
|
||
| set status = ##class(%IPM.Main).Shell("uninstall "_..#CommonPathPrefix) | ||
| do $$$AssertStatusOK(status,"uninstalled "_..#CommonPathPrefix_" module successfully.") | ||
| } | ||
|
|
||
| Method VerifyCORSSettings() | ||
| { | ||
| 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)&&(corsAllowlist'="") { | ||
| do $$$AssertStatusOK(1,"CorsAllowlist values are defined") | ||
| do $$$LogMessage(corsAllowlist) | ||
| } | ||
| if $data(props("CorsCredentialsAllowed"),corsAllow)&&(corsAllow'="") { | ||
| do $$$AssertStatusOK(1,"CorsCredentialsAllowed values are defined") | ||
| do $$$LogMessage(corsAllow) | ||
| } | ||
| if $data(props("CorsHeadersList"),corsHeadersList)&&(corsHeadersList'="") { | ||
| do $$$AssertStatusOK(1,"CorsHeadersList values are defined") | ||
| do $$$LogMessage(corsHeadersList) | ||
| } | ||
| } | ||
|
|
||
| XData ModuleWithWebApTag [ MimeType = application/xml ] | ||
| { | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <Export generator="Cache" version="25"> | ||
| <Document name="cors-rest-apps.ZPM"> | ||
| <Module> | ||
| <Name>cors-rest-apps</Name> | ||
| <Version>1.0.0</Version> | ||
| <Description>cors enabling testing on webapplication</Description> | ||
| <Keywords>cors</Keywords> | ||
| <Packaging>module</Packaging> | ||
| <WebApplication Url="/testcors" CookiePath="/testcors" | ||
| CorsAllowlist="https://pm.intersystems.com" | ||
| CorsCredentialsAllowed="1" | ||
| 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" | ||
| PasswordAuthEnabled="1" Recurse="1" UnauthenticatedEnabled="0" UseCookies="2"/> | ||
|
||
| <LifecycleClass>%IPM.Lifecycle.Module</LifecycleClass> | ||
| <SourcesRoot>src</SourcesRoot> | ||
| </Module> | ||
| </Document> | ||
| </Export> | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: use
ifinstead of#ifUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the code based on the comment