Browse Source

Merge pull request #14210 from dcamper/jwt-authz

HPCC-24567 JWT-based security manager plugin

Reviewed-by: Gavin Halliday <ghalliday@hpccsystems.com>
Gavin Halliday 4 years ago
parent
commit
4842b7ce6e
31 changed files with 5478 additions and 31 deletions
  1. 3 0
      .gitmodules
  2. 22 8
      dali/server/daldap.cpp
  3. 1 0
      dali/server/daldap.hpp
  4. 60 11
      dali/server/daserver.cpp
  5. 1 1
      dockerfiles/platform-core-debug/esp.xml
  6. 1 1
      dockerfiles/platform-core/esp.xml
  7. 5 0
      esp/applications/eclservices/eclservices.yaml
  8. 5 0
      esp/applications/eclwatch/eclwatch.yaml
  9. 1 1
      esp/bindings/http/platform/httpbinding.cpp
  10. 6 0
      initfiles/componentfiles/configschema/xsd/dali.xsd
  11. 6 0
      initfiles/componentfiles/configxml/@temp/esp_service_WsSMC.xsl
  12. 8 0
      initfiles/componentfiles/configxml/dali.xsd
  13. 27 2
      initfiles/componentfiles/configxml/dali.xsl
  14. 3 0
      system/security/CMakeLists.txt
  15. 1 0
      system/security/plugins/jwtSecurity/.gitignore
  16. 60 0
      system/security/plugins/jwtSecurity/CMakeLists.txt
  17. 2604 0
      system/security/plugins/jwtSecurity/Doxyfile
  18. 168 0
      system/security/plugins/jwtSecurity/README.md
  19. 24 0
      system/security/plugins/jwtSecurity/configxml/CMakeLists.txt
  20. 32 0
      system/security/plugins/jwtSecurity/configxml/buildset.xml
  21. 1 0
      system/security/plugins/jwtSecurity/configxml/genenvrules.conf
  22. 145 0
      system/security/plugins/jwtSecurity/configxml/jwtsecmgr.xsd
  23. 1 0
      system/security/plugins/jwtSecurity/jwt-cpp
  24. 397 0
      system/security/plugins/jwtSecurity/jwtCache.cpp
  25. 455 0
      system/security/plugins/jwtSecurity/jwtCache.hpp
  26. 187 0
      system/security/plugins/jwtSecurity/jwtEndpoint.cpp
  27. 61 0
      system/security/plugins/jwtSecurity/jwtEndpoint.hpp
  28. 1148 0
      system/security/plugins/jwtSecurity/jwtSecurity.cpp
  29. 36 0
      system/security/plugins/jwtSecurity/jwtSecurity.hpp
  30. 2 1
      system/security/shared/seclib.hpp
  31. 7 6
      system/security/shared/secloader.hpp

+ 3 - 0
.gitmodules

@@ -58,3 +58,6 @@
 [submodule "plugins/spark/spark-plugin-java-packages"]
 	path = plugins/spark/spark-plugin-java-packages
 	url = https://github.com/hpcc-systems/spark-plugin-java-packages.git
+[submodule "system/security/plugins/jwtSecurity/jwt-cpp"]
+	path = system/security/plugins/jwtSecurity/jwt-cpp
+	url = https://github.com/hpcc-systems/jwt-cpp.git

+ 22 - 8
dali/server/daldap.cpp

@@ -32,6 +32,7 @@ using namespace cryptohelper;
 
 #ifndef _NO_LDAP
 #include "seclib.hpp"
+#include "secloader.hpp"
 #include "ldapsecurity.hpp"
 
 static void ignoreSigPipe()
@@ -73,7 +74,7 @@ class CDaliLdapConnection: implements IDaliLdapConnection, public CInterface
         catch (IException *e) {
             EXCLOG(e,"LDAP createDefaultScopes");
             throw;
-        }   
+        }
     }
 
 
@@ -115,7 +116,7 @@ public:
                 catch (IException *e) {
                     EXCLOG(e,"LDAP server");
                     throw;
-                }   
+                }
                 createDefaultScopes();
             }
         }
@@ -124,11 +125,11 @@ public:
 
     SecAccessFlags getPermissions(const char *key,const char *obj,IUserDescriptor *udesc,unsigned auditflags)
     {
-        if (!ldapsecurity||((getLDAPflags()&DLF_ENABLED)==0)) 
+        if (!ldapsecurity||((getLDAPflags()&DLF_ENABLED)==0))
             return SecAccess_Full;
         StringBuffer username;
         StringBuffer password;
-        if (udesc) 
+        if (udesc)
         {
             udesc->getUserName(username);
             udesc->getPassword(password);
@@ -163,16 +164,16 @@ public:
 
             unsigned taken = msTick()-start;
 #ifndef _DEBUG
-            if (taken>100) 
+            if (taken>100)
 #endif
             {
                 PROGLOG("LDAP: getPermissions(%s) scope=%s user=%s returns %d in %d ms",key?key:"NULL",obj?obj:"NULL",username.str(),perm,taken);
             }
             if (auditflags&DALI_LDAP_AUDIT_REPORT) {
                 StringBuffer auditstr;
-                if ((auditflags&DALI_LDAP_READ_WANTED)&&!HASREADPERMISSION(perm)) 
+                if ((auditflags&DALI_LDAP_READ_WANTED)&&!HASREADPERMISSION(perm))
                     auditstr.append("Lookup Access Denied");
-                else if ((auditflags&DALI_LDAP_WRITE_WANTED)&&!HASWRITEPERMISSION(perm)) 
+                else if ((auditflags&DALI_LDAP_WRITE_WANTED)&&!HASWRITEPERMISSION(perm))
                     auditstr.append("Create Access Denied");
                 if (auditstr.length()) {
                     auditstr.append(":\n\tProcess:\tdaserver");
@@ -262,7 +263,7 @@ public:
     {
         return ldapflags;
     }
-    
+
     void setLDAPflags(unsigned flags)
     {
         ldapflags = flags;
@@ -272,6 +273,19 @@ public:
 };
 
 
+IDaliLdapConnection *createDaliSecMgrPluginConnection(IPropertyTree *propTree)
+{
+    if (propTree && propTree->hasProp("@type"))
+    {
+        IPropertyTree* secMgrCfg = propTree->queryPropTree(propTree->queryProp("@type"));
+        return SecLoader::loadPluggableSecManager<IDaliLdapConnection>("dali", propTree, secMgrCfg);
+    }
+    else
+    {
+        return nullptr;
+    }
+}
+
 IDaliLdapConnection *createDaliLdapConnection(IPropertyTree *proptree)
 {
     return new CDaliLdapConnection(proptree);

+ 1 - 0
dali/server/daldap.hpp

@@ -34,6 +34,7 @@ interface IDaliLdapConnection: extends IInterface
     virtual bool enableScopeScans(IUserDescriptor *udesc, bool enable, int *err) = 0;
 };
 
+extern IDaliLdapConnection *createDaliSecMgrPluginConnection(IPropertyTree *proptree);
 extern IDaliLdapConnection *createDaliLdapConnection(IPropertyTree *proptree);
 
 

+ 60 - 11
dali/server/daserver.cpp

@@ -126,7 +126,7 @@ bool actionOnAbort()
 {
     stopServer();
     return true;
-} 
+}
 
 USE_JLIB_ALLOC_HOOK;
 
@@ -139,6 +139,46 @@ void usage(void)
     printf("--daemon|-d <instanceName>\t: run daemon as instance\n");
 }
 
+static IPropertyTree *getSecMgrPluginPropTree(const IPropertyTree *configTree)
+{
+    Owned<IPropertyTree> foundTree;
+
+#ifdef _CONTAINERIZED
+    // TODO
+#else
+    Owned<IRemoteConnection> conn = querySDS().connect("/Environment", 0, 0, INFINITE);
+
+    if (conn)
+    {
+        const IPropertyTree *proptree = conn->queryRoot()->queryPropTree("Software/DaliServerProcess[1]");
+
+        if (proptree)
+        {
+            const char* authMethod = proptree->queryProp("@authMethod");
+
+            if (strisame(authMethod, "secmgrPlugin"))
+            {
+                const char* authPluginType = proptree->queryProp("@authPluginType");
+
+                if (authPluginType)
+                {
+                    VStringBuffer xpath("SecurityManagers/SecurityManager[@name='%s']", authPluginType);
+
+                    foundTree.setown(configTree->getPropTree(xpath));
+
+                    if (!foundTree.get())
+                    {
+                        WARNLOG("secmgPlugin '%s' not defined in configuration", authPluginType);
+                    }
+                }
+            }
+        }
+    }
+#endif
+
+    return foundTree.getClear();
+}
+
 /* NB: Ideally this belongs within common/environment,
  * however, that would introduce a circular dependency.
  */
@@ -387,7 +427,7 @@ int main(int argc, const char* argv[])
         Owned<IFile> sentinelFile = createSentinelTarget();
         removeSentinelFile(sentinelFile);
 #ifndef _CONTAINERIZED
-	
+
         for (unsigned i=1;i<(unsigned)argc;i++) {
             if (streq(argv[i],"--daemon") || streq(argv[i],"-d")) {
                 if (daemon(1,0) || write_pidfile(argv[++i])) {
@@ -449,10 +489,10 @@ int main(int argc, const char* argv[])
             }
         }
         // JCSMORE remoteBackupLocation should not be a property of SDS section really.
-        if (!getConfigurationDirectory(serverConfig->queryPropTree("Directories"),"mirror","dali",serverConfig->queryProp("@name"),mirrorPath)) 
+        if (!getConfigurationDirectory(serverConfig->queryPropTree("Directories"),"mirror","dali",serverConfig->queryProp("@name"),mirrorPath))
             serverConfig->getProp("SDS/@remoteBackupLocation",mirrorPath);
 
-#endif            
+#endif
         if (dataPath.length())
         {
             addPathSepChar(dataPath); // ensures trailing path separator
@@ -508,8 +548,8 @@ int main(int argc, const char* argv[])
                         if (mirrorPath.length()<=2 || !isPathSepChar(mirrorPath.charAt(0)) || !isPathSepChar(mirrorPath.charAt(1)))
                             rfn.setLocalPath(mirrorPath.str());
                         else
-                            rfn.setRemotePath(mirrorPath.str());                
-                        
+                            rfn.setRemotePath(mirrorPath.str());
+
                         if (!rfn.getPort() && !rfn.isLocal())
                         {
                             StringBuffer mountPoint;
@@ -545,7 +585,7 @@ int main(int argc, const char* argv[])
                         OwnedIFile iFileBackup = createIFile(backupCheck.str());
                         if (iFileBackup->exists())
                         {
-                            PROGLOG("remoteBackupLocation and dali data path point to same location! : %s", mirrorPath.str()); 
+                            PROGLOG("remoteBackupLocation and dali data path point to same location! : %s", mirrorPath.str());
                             iFileDataDir->remove();
                             return 0;
                         }
@@ -619,10 +659,10 @@ int main(int argc, const char* argv[])
 #ifndef _CONTAINERIZED
         setMsgLevel(fileMsgHandler, serverConfig->getPropInt("SDS/@msgLevel", 100));
 #endif
-        startLogMsgChildReceiver(); 
+        startLogMsgChildReceiver();
         startLogMsgParentReceiver();
 
-        IGroup *group = createIGroup(epa); 
+        IGroup *group = createIGroup(epa);
         initCoven(group,serverConfig);
         group->Release();
         epa.kill();
@@ -641,7 +681,7 @@ int main(int argc, const char* argv[])
             auditDir.set(lf->queryLogDir());
         }
 
-// SNMP logging     
+// SNMP logging
         bool enableSNMP = serverConfig->getPropBool("SDS/@enableSNMP");
         if (serverConfig->getPropBool("SDS/@enableSysLog",true))
             UseSysLogForOperatorMessages();
@@ -675,7 +715,16 @@ int main(int argc, const char* argv[])
         }
         try {
 #ifndef _NO_LDAP
-            setLDAPconnection(createDaliLdapConnection(serverConfig->getPropTree("Coven/ldapSecurity")));
+            Owned<IPropertyTree> secMgrPropTree = getSecMgrPluginPropTree(serverConfig);
+
+            if (secMgrPropTree.get())
+            {
+                setLDAPconnection(createDaliSecMgrPluginConnection(secMgrPropTree));
+            }
+            else
+            {
+                setLDAPconnection(createDaliLdapConnection(serverConfig->getPropTree("Coven/ldapSecurity")));
+            }
 #endif
         }
         catch (IException *e) {

+ 1 - 1
dockerfiles/platform-core-debug/esp.xml

@@ -62,7 +62,7 @@
       <EspBinding name="ws_esdlconfig_myespsmc_myesp" service="ws_esdlconfig_EclWatch_myesp" protocol="http" type="ws_esdlconfigSoapBinding" plugin="ws_esdlconfig" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>
       <EspService name="ws_elk_EclWatch_myesp" type="ws_elk" plugin="ws_elk"><ELKIntegration><Kibana/><ElasticSearch/><LogStash/></ELKIntegration></EspService>
       <EspBinding name="ws_elk_myespsmc_myesp" service="ws_elk_EclWatch_myesp" protocol="http" type="ws_elkSoapBinding" plugin="ws_elk" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>
-      <EspService name="ws_store_EclWatch_myesp" type="ws_store" plugin="ws_store"><StoreProvider lib="dalistorelib"/><Stores><Store description="Generic KeyVal store for HPCC Applications" name="HPCCApps" default="true"/></Stores></EspService>
+      <EspService name="ws_store_EclWatch_myesp" type="ws_store" plugin="ws_store"><StoreProvider lib="dalistorelib"/><Stores><Store description="Generic KeyVal store for HPCC Applications" name="HPCCApps" default="true"/><Store description="JWT token cache" name="JWTAuth" default="false" maxValSize="32768"/></Stores></EspService>
       <EspBinding name="ws_store_myespsmc_myesp" service="ws_store_EclWatch_myesp" protocol="http" type="ws_storeSoapBinding" plugin="ws_store" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>
       <EspService name="ws_codesign_EclWatch_myesp" type="ws_codesign" plugin="ws_codesign"/>
       <EspBinding name="ws_codesign_myespsmc_myesp" service="ws_codesign_EclWatch_myesp" protocol="http" type="ws_codesignSoapBinding" plugin="ws_codesign" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>

+ 1 - 1
dockerfiles/platform-core/esp.xml

@@ -62,7 +62,7 @@
       <EspBinding name="ws_esdlconfig_myespsmc_myesp" service="ws_esdlconfig_EclWatch_myesp" protocol="http" type="ws_esdlconfigSoapBinding" plugin="ws_esdlconfig" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>
       <EspService name="ws_elk_EclWatch_myesp" type="ws_elk" plugin="ws_elk"><ELKIntegration><Kibana/><ElasticSearch/><LogStash/></ELKIntegration></EspService>
       <EspBinding name="ws_elk_myespsmc_myesp" service="ws_elk_EclWatch_myesp" protocol="http" type="ws_elkSoapBinding" plugin="ws_elk" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>
-      <EspService name="ws_store_EclWatch_myesp" type="ws_store" plugin="ws_store"><StoreProvider lib="dalistorelib"/><Stores><Store description="Generic KeyVal store for HPCC Applications" name="HPCCApps" default="true"/></Stores></EspService>
+      <EspService name="ws_store_EclWatch_myesp" type="ws_store" plugin="ws_store"><StoreProvider lib="dalistorelib"/><Stores><Store description="Generic KeyVal store for HPCC Applications" name="HPCCApps" default="true"/><Store description="JWT token cache" name="JWTAuth" default="false" maxValSize="32768"/></Stores></EspService>
       <EspBinding name="ws_store_myespsmc_myesp" service="ws_store_EclWatch_myesp" protocol="http" type="ws_storeSoapBinding" plugin="ws_store" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>
       <EspService name="ws_codesign_EclWatch_myesp" type="ws_codesign" plugin="ws_codesign"/>
       <EspBinding name="ws_codesign_myespsmc_myesp" service="ws_codesign_EclWatch_myesp" protocol="http" type="ws_codesignSoapBinding" plugin="ws_codesign" netAddress="0.0.0.0" port="8010" wsdlServiceAddress="" defaultServiceVersion=""/>

+ 5 - 0
esp/applications/eclservices/eclservices.yaml

@@ -60,6 +60,11 @@ eclservices:
          -  name: HPCCApps
             description: Generic KeyVal store for HPCC Applications
             default: true
+         Store:
+         -  name: JWTAuth
+            description: JWT token cache
+            default: false
+            maxValSize: 32768
    ws_machine:
       excludePartitions: "/dev*,/sys*,/proc*,/run*,/boot"
       monitorDaliFileServer: false

+ 5 - 0
esp/applications/eclwatch/eclwatch.yaml

@@ -58,6 +58,11 @@ eclwatch:
          -  name: HPCCApps
             description: Generic KeyVal store for HPCC Applications
             default: true
+         Store:
+         -  name: JWTAuth
+            description: JWT token cache
+            default: false
+            maxValSize: 32768
    ws_machine:
       excludePartitions: "/dev*,/sys*,/proc*,/run*,/boot"
       monitorDaliFileServer: false

+ 1 - 1
esp/bindings/http/platform/httpbinding.cpp

@@ -197,7 +197,7 @@ EspHttpBinding::EspHttpBinding(IPropertyTree* tree, const char *bindname, const
                 if (secMgrCfg)
                 {
                     //This is a Pluggable Security Manager
-                    m_secmgr.setown(SecLoader::loadPluggableSecManager(bindname, bnd_cfg, secMgrCfg));
+                    m_secmgr.setown(SecLoader::loadPluggableSecManager<ISecManager>(bindname, bnd_cfg, secMgrCfg));
                     m_authmap.setown(m_secmgr->createAuthMap(authcfg));
                     m_feature_authmap.setown(m_secmgr->createFeatureMap(authcfg));
                     m_setting_authmap.setown(m_secmgr->createSettingMap(authcfg));

+ 6 - 0
initfiles/componentfiles/configschema/xsd/dali.xsd

@@ -104,9 +104,15 @@
                             <xs:restriction base="xs:string">
                                 <xs:enumeration value="kerberos" hpcc:description=""/>
                                 <xs:enumeration value="simple" hpcc:description=""/>
+                                <xs:enumeration value="secmgrPlugin" hpcc:description=""/>
                             </xs:restriction>
                         </xs:simpleType>
                     </xs:attribute>
+                    <xs:attribute name="authPluginType" type="xs:string"
+                                  hpcc:displayName="Security Manager Plugin Name"
+                                  hpcc:visibleIf="/Environment/Software/LDAPServerProcess"
+                                  hpcc:requiredIf=".[@authMethod=('secmgrPlugin')]"
+                                  hpcc:tooltip="Security Manager plugin name (when authentication method is secmgrPlugin)"/>
                     <xs:attribute name="filesDefaultUser" type="xs:string" hpcc:displayName="Files Default User"
                                   hpcc:visibleIf="/Environment/Software/LDAPServerProcess"
                                   hpcc:tooltip="The default username for Files access (ActiveDirectory)"/>

+ 6 - 0
initfiles/componentfiles/configxml/@temp/esp_service_WsSMC.xsl

@@ -675,6 +675,12 @@ This is required by its binding with ESP service '<xsl:value-of select="$espServ
                     <xsl:attribute name="name">HPCCApps</xsl:attribute>
                     <xsl:attribute name="default">true</xsl:attribute>
                 </xsl:element>
+                <xsl:element name="Store">
+                    <xsl:attribute name="description">JWT token cache</xsl:attribute>
+                    <xsl:attribute name="name">JWTAuth</xsl:attribute>
+                    <xsl:attribute name="default">false</xsl:attribute>
+                    <xsl:attribute name="maxValSize">32768</xsl:attribute>
+                </xsl:element>
             </xsl:element>
         </EspService>
         <EspBinding name="{$bindName}" service="{$serviceName}" protocol="{$bindingNode/@protocol}" type="{$bindType}"

+ 8 - 0
initfiles/componentfiles/configxml/dali.xsd

@@ -384,9 +384,17 @@
         <xs:restriction base="xs:string">
           <xs:enumeration value="kerberos"/>
           <xs:enumeration value="simple"/>
+          <xs:enumeration value="secmgrPlugin"/>
         </xs:restriction>
       </xs:simpleType>
     </xs:attribute>
+    <xs:attribute name="authPluginType" use="optional" type="xs:string">
+      <xs:annotation>
+        <xs:appinfo>
+          <tooltip>Security Manager plugin name (when authentication method is secmgrPlugin).</tooltip>
+        </xs:appinfo>
+      </xs:annotation>
+    </xs:attribute>
     <xs:attribute name="filesDefaultUser" use="optional" type="xs:string">
       <xs:annotation>
         <xs:appinfo>

+ 27 - 2
initfiles/componentfiles/configxml/dali.xsl

@@ -81,7 +81,7 @@
     <DALI>
       <xsl:attribute name="name">
         <xsl:value-of select="@name"/>
-      </xsl:attribute> 
+      </xsl:attribute>
       <xsl:if test="string(@LogDir)!=''">
         <xsl:attribute name="log_dir">
           <xsl:value-of select="@LogDir"/>
@@ -99,7 +99,32 @@
           </xsl:call-template>
         </xsl:attribute>
       </xsl:if>
-      <xsl:copy-of select="/Environment/Software/Directories"/>  
+      <xsl:copy-of select="/Environment/Software/Directories"/>
+
+      <xsl:if test="@authMethod='secmgrPlugin'">
+      <SecurityManagers>
+          <SecurityManager>
+              <xsl:variable name="instanceName" select="@authPluginType"/>
+              <xsl:if test="not(/Environment/Software/*[@name=$instanceName and @type='SecurityManager'])">
+                  <xsl:message terminate="yes">Security Manager instance of name <xsl:value-of select="@authPluginType"/> is referenced in service <xsl:value-of select="@name"/> of ESP <xsl:value-of select="../@name"/> but does not exist"</xsl:message>
+              </xsl:if>
+              <xsl:attribute name="name">
+                  <xsl:value-of select="/Environment/Software/*[@name=$instanceName and @type='SecurityManager']/@name"/>
+              </xsl:attribute>
+              <xsl:attribute name="instanceFactoryName">
+                  <xsl:value-of select="/Environment/Software/*[@name=$instanceName and @type='SecurityManager']/@instanceFactoryName"/>
+              </xsl:attribute>
+              <xsl:attribute name="libName">
+                  <xsl:value-of select="/Environment/Software/*[@name=$instanceName and @type='SecurityManager']/@libName"/>
+              </xsl:attribute>
+              <xsl:attribute name="type">
+                  <xsl:value-of select="name(/Environment/Software/*[@name=$instanceName and @type='SecurityManager'])"/>
+              </xsl:attribute>
+              <xsl:copy-of select="/Environment/Software/*[@name=$instanceName and @type='SecurityManager']"/>
+          </SecurityManager>
+      </SecurityManagers>
+     </xsl:if>
+
       <xsl:element name="SDS">
         <xsl:attribute name="store">dalisds.xml</xsl:attribute>
         <xsl:attribute name="caseInsensitive">0</xsl:attribute>

+ 3 - 0
system/security/CMakeLists.txt

@@ -28,3 +28,6 @@ IF (USE_APR)
   HPCC_ADD_SUBDIRECTORY (plugins/htpasswdSecurity)
 ENDIF(USE_APR)
 HPCC_ADD_SUBDIRECTORY (plugins/singleuserSecurity)
+IF (NOT WIN32)
+  HPCC_ADD_SUBDIRECTORY (plugins/jwtSecurity)
+ENDIF()

+ 1 - 0
system/security/plugins/jwtSecurity/.gitignore

@@ -0,0 +1 @@
+docs/

+ 60 - 0
system/security/plugins/jwtSecurity/CMakeLists.txt

@@ -0,0 +1,60 @@
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2020 HPCC Systems®.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+
+project(jwtSecurity)
+
+# configmgr directives
+add_subdirectory(configxml)
+
+# https://github.com/Thalhammer/jwt-cpp; disable tests and examples
+option(BUILD_TESTS "Configure CMake to build tests (or not)" ON) # Copy from jwt-cpp/CMakeLists.txt
+set(BUILD_TESTS OFF)
+option(BUILD_EXAMPLES "Configure CMake to build examples (or not)" ON) # Copy from jwt-cpp/CMakeLists.txt
+set(BUILD_EXAMPLES OFF)
+add_subdirectory(jwt-cpp EXCLUDE_FROM_ALL)
+
+# Required installed libraries
+find_package(CURL REQUIRED)
+
+set(SRCS
+    "${CMAKE_CURRENT_SOURCE_DIR}/jwtSecurity.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/jwtCache.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/jwtEndpoint.cpp"
+    "${HPCC_SOURCE_DIR}/system/security/shared/authmap.cpp"
+    "${HPCC_SOURCE_DIR}/esp/services/ws_store/espstorelib/daliKVStore.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/jwt-cpp/include/jwt-cpp/jwt.h")
+
+add_definitions(-DJWTSECURITY_EXPORTS -D_USRDLL)
+
+include_directories("${CMAKE_CURRENT_SOURCE_DIR}/jwt-cpp/include"
+                    "${HPCC_SOURCE_DIR}/dali/base"
+                    "${HPCC_SOURCE_DIR}/dali/server"
+                    "${HPCC_SOURCE_DIR}/esp/services/ws_store/espstorelib"
+                    "${HPCC_SOURCE_DIR}/system/include"
+                    "${HPCC_SOURCE_DIR}/system/mp"
+                    "${HPCC_SOURCE_DIR}/system/security/shared"
+                    "${HPCC_SOURCE_DIR}/system/jlib"
+                    "${CURL_INCLUDE_DIR}")
+
+HPCC_ADD_LIBRARY(jwtSecurity SHARED ${SRCS})
+
+install(TARGETS jwtSecurity
+        RUNTIME DESTINATION "${EXEC_DIR}"
+        LIBRARY DESTINATION "${LIB_DIR}")
+
+target_link_libraries(jwtSecurity
+                      PRIVATE jlib dalibase
+                      PRIVATE ${CURL_LIBRARIES})

File diff suppressed because it is too large
+ 2604 - 0
system/security/plugins/jwtSecurity/Doxyfile


+ 168 - 0
system/security/plugins/jwtSecurity/README.md

@@ -0,0 +1,168 @@
+## JWT Authorization Security Manager Plugin
+
+The purpose of this plugin is to provide authentication and authorization capabilities for HPCC Systems users, with the
+credentials passed via valid JWT tokens.
+
+The intention is to adhere as closely as possibly to the OpenID Connect (OIC) specification, which is a simple identity
+layer on top of the OAuth 2.0 protocol, while maintaining compatibility with the way HPCC Systems performs
+authentication and authorization today.  More information about the OpenID Connect specification can be found at
+<https://openid.net/specs/openid-connect-core-1_0.html>.
+
+One of the big advantages of OAuth 2.0 and OIC is that the service (in this case, HPCC Systems) never interacts with the
+user directly.  Instead, authentication is performed by a trusted third party and the (successful) results are passed to
+the service in the form of a verifiable encoded token.
+
+Unfortunately, HPCC Systems does not support the concept of third-party verification.  It assumes that users -- really,
+any client application that operates as a user, including things like IDEs -- will submit username/password credentials
+for authentication.  Until that is changed, HPCC Systems won't be able to fully adhere to the OIC specification.
+
+We can, however, implement *most* of the specification.  That is what this plugin does.
+
+NOTE: This plugin is not available in a Windows build.
+
+### Code Documentation
+
+Doxygen (<https://www.doxygen.nl/index.html>) can be used to create nice HTML documentation for the code.  Call/caller
+graphs are also generated for functions if you have dot (https://www.graphviz.org/download/) installed and available on
+your path.
+
+Assuming ```doxygen``` is on your path, you can build the documentation via:
+
+	cd system/security/plugins/jwtSecurity
+	doxygen Doxyfile
+
+The documentation can then be accessed via ```docs/html/index.html```.
+
+### Theory of Operations
+
+The plugin is called by the HPCC Systems ```esp``` process when a user needs to be authenticated.  That call will
+contain the user's username and either a reference to a session token or a password.  The session token is present only
+for already-authenticated users.
+
+If the session token is not present, the plugin will call a ```JWT login service``` (also known as a JWT login endpoint)
+with the username and password, plus a nonce value for additional security.
+
+That service authenticates the username/password credentials.  If everything is good, the service constructs an
+OIC-compatible token that includes authorization information for that user and returns it to the plugin.  The
+token is validated according to the OIC specification, including signature verification.
+
+Note that token signature verification requires an additional piece of information.  Tokens can be signed with a
+hash-based algorithm or with a public key-based algorithm (the actual algorithm used is determined by the JWT service). 
+To verify either kind of algorithm, the plugin will need either the secret hash key or the public key that matches what
+the JWT service used.  That key is read by the plugin from a file, and the file is determined by a configuration setting
+(see below).  It is possible to change the contents of that file without restarting the esp process.  Note, though, that
+the plugin may not notice that the file's contents have changed for several seconds (changes do not **immediately** take
+effect).
+
+HPCC Systems uses a well-defined authorization scheme, originally designed around an LDAP implementation.  That scheme
+is represented within the token as JWT claims.  This plugin will unpack those claims and map to the authorization checks
+already in place within the HPCC Systems platform.
+
+OIC includes the concept of refresh tokens.  Refresh tokens enable a service to re-authorize an existing token without
+user intervention.  Re-authorization typically happens due to a token expiring.  Tokens should have a
+relatively short lifetime -- e.g. 15-30 minutes -- to promote good security and also give administrators the ability to
+modify a user's authorization while the user is logged in.  This plugin fully supports refresh tokens by validating
+token lifetime at every authorization check and calling a ```JWT refresh service``` (also known as a JWT refresh
+endpoint) when needed.  This largely follows the OIC specification.
+
+### Deviations From OIC Specification
+
+* Initial authentication:  As stated above, the ```esp``` process will gather the username/password credentials instead
+of a third party, then send those credentials off to another service.  In a true OIC configuration, the client process
+(the ```esp``` process) never sees user credentials and relies on an external service to gather them from the user.
+* The request made to the ```JWT login service``` is a POST HTTP or HTTPS call (depending on your configuration)
+containing four items in JSON format; example:
+
+```
+	{
+		"username": "my_username",
+		"password": "my_password",
+		"client_id": "https://myhpcccluster.com",
+		"nonce": "hf674DTRMd4Z1s"
+	}
+```
+* The ```JWT login service``` should reply with an OIC-compatible JSON-formatted reply.  See
+<https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse> for an example of a successful authentication and
+<https://openid.net/specs/openid-connect-core-1_0.html#TokenErrorResponse> for an example of an error response.  The
+token itself is comprised of several attributes, followed by HPCC Systems-specific claims; see
+<https://openid.net/specs/openid-connect-core-1_0.html#IDToken> (all required fields are indeed required, plus the nonce
+field).
+	* Note that for success replies, the ```access_token``` and ```expires_in``` values are ignored by this plugin.
+* If the token expires, the plugin will call the ```JWT refresh service``` to request a new token.  This follows
+<https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens> except that the ```client_secret``` and ```scope```
+fields in the request are omitted.
+
+### Implications of Deviations
+
+The most obvious outcome of this implementation is that a custom service/endpoint needs to be available.  Or rather two
+services:  One to handle the initial user login and one to handle token refreshes.  Neither service *precisely* handles
+requests and replies in an OIC-compatible way, but the tokens themselves *are* OIC-compatible, which is good.  That
+allows you to use third-party JWT libraries to construct and validate those tokens.
+
+### HPCC Systems Configuration Notes
+
+Several items must be defined in the platform's configuration.  Within configmgr, the ```jwtsecmgr``` Security Manager
+plugin must be added as a component and then modified according to your environment:
+
+* The URL or unique name of this HPCC Systems cluster, used as the ```client_id``` in token requests
+* Full URL to the ```JWT Login Endpoint``` (should be HTTPS, but not required)
+* Full URL to the ```JWT Refresh Endpoint``` (should be HTTPS, but not required)
+* Boolean indicating whether to accept self-signed certificates for those endpoints; defaults to false
+* Secrets vault key/name or subdirectory under /opt/HPCCSystems/secrets/system in which the JWT key used for the chosen signature algorithm is stored; defaults to "jwt-security"
+* Default permission access level (either "Full" or "None"); defaults to "Full"
+* Default workunit scope access level (either "Full" or "None"); defaults to "Full"
+* Default file scope access level (either "Full" or "None"); defaults to "Full"
+
+Only the first three items have no default values and must be supplied.
+
+Once the ```jwtseccmgr``` component is added, you have to tell other parts of the system to use the plugin.  For
+user authentication and permissions affecting features and workunit scopes, you need to add the plugin to the ```esp```
+component.  Instructions for doing so can be found in the HPCC Systems Administrator's Guide manual (though the
+manual uses the htpasswd plugin as an example, the process is the same).
+
+If you intend to implement file scope permissions then you will also need provide Dali information about the JWT
+plugin.  In configmgr, within the ```Dali Server``` component, select the LDAP tab.  Change the ```authMethod``` entry
+to ```secmgrPlugin``` and enter "jwtsecmgr" as the ```authPluginType```.  Make sure ```checkScopeScans``` is set to
+true.
+
+### HPCC Systems Authorization and JWT Claims
+
+This plugin supports all authorizations documented in the HPCC Systems® Administrator's Guide with the exception of
+"View Permissions".  Loosely speaking, the permissions are divided into three groups:  Feature, Workunit Scope, and File
+Scope.
+
+Feature permissions are supported exactly as documented.  A specific permission would exist as a JWT claim, by name,
+with the associated value being the name of the permission.  For example, to grant read-only access to ECL Watch, use
+this claim:
+
+```
+	{ "SmcAccess": "Read" }
+```
+
+File and workunit scope permissions are handled the same way, but different from feature permissions.  The claim is one
+of the Claim constants in the tables below, and the associated value is a matching pattern.  A pattern can be simple
+string or it can use wildcards (specifically, Linux's file globbing wildcards).  Wildcards are not typically needed.
+
+Multiple patterns can be set for each claim.
+
+#### Workunit Scope Permissions
+
+Meaning|Claim|Value
+-------|-----|-----
+User has view rights to workunit scope|AllowWorkunitScopeView|*pattern*
+User has modify rights to workunit scope|AllowWorkunitScopeModify|*pattern*
+User has delete rights to workunit scope|AllowWorkunitScopeDelete|*pattern*
+User does not have view rights to workunit scope|DenyWorkunitScopeView|*pattern*
+User does not have modify rights to workunit scope|DenyWorkunitScopeModify|*pattern*
+User does not have delete rights to workunit scope|DenyWorkunitScopeDelete|*pattern*
+
+#### File Scope Permissions
+
+Meaning|Claim|Value
+-------|-----|-----
+User has view rights to file scope|AllowFileScopeView|*pattern*
+User has modify rights to file scope|AllowFileScopeModify|*pattern*
+User has delete rights to file scope|AllowFileScopeDelete|*pattern*
+User does not have view rights to file scope|DenyFileScopeView|*pattern*
+User does not have modify rights to file scope|DenyFileScopeModify|*pattern*
+User does not have delete rights to file scope|DenyFileScopeDelete|*pattern*

+ 24 - 0
system/security/plugins/jwtSecurity/configxml/CMakeLists.txt

@@ -0,0 +1,24 @@
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2016 HPCC Systems.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+
+install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/jwtsecmgr.xsd"
+        DESTINATION componentfiles/configxml
+        COMPONENT RUNTIME)
+
+install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/buildset.xml"
+              "${CMAKE_CURRENT_SOURCE_DIR}/genenvrules.conf"
+        DESTINATION componentfiles/configxml/plugins/jwtsecmgr
+        COMPONENT RUNTIME)

+ 32 - 0
system/security/plugins/jwtSecurity/configxml/buildset.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2016 HPCC Systems.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+-->
+<Environment>
+  <Programs>
+    <Build name="_" url="/opt/HPCCSystems">
+      <BuildSet deployable="no"
+                installSet="deploy_map.xml"
+                name="jwtsecmgr"
+                path="componentfiles/jwtsecmgr"
+                processName="JwtSecurityManager"
+                schema="jwtsecmgr.xsd">
+      </BuildSet>
+    </Build>
+  </Programs>
+</Environment>
+

+ 1 - 0
system/security/plugins/jwtSecurity/configxml/genenvrules.conf

@@ -0,0 +1 @@
+do_not_generate=jwtsecmgr

+ 145 - 0
system/security/plugins/jwtSecurity/configxml/jwtsecmgr.xsd

@@ -0,0 +1,145 @@
+<!--
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2020 HPCC Systems.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+-->
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
+  <xs:element name="jwtsecmgr">
+    <xs:complexType>
+
+      <xs:attribute name="loginEndpoint" type="xs:string" use="required">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Full URL of the JWT login endpoint</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <xs:attribute name="refreshEndpoint" type="xs:string" use="required">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Full URL of the JWT refresh endpoint</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <xs:attribute name="allowSelfSignedCert" type="xs:boolean" use="required" default="false">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Allow endpoints to use a self-signed TLS certificate</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <xs:attribute name="secretsName" type="xs:string" use="required">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Secrets vault name or subdirectory under /opt/HPCCSystems/secrets/systems in which the JWT key used for the chosen signature algorithm (hash key, for HSxxx algorithms; public key for the others) is stored</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <xs:attribute name="clientID" type="xs:string" use="required">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>URL or unique name of this HPCC Systems cluster</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <xs:attribute name="permDefaultAccess" use="required" default="Full">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Default access level for permissions; default value is 'Full'</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+        <xs:simpleType>
+          <xs:restriction base="xs:string">
+            <xs:enumeration value="None"/>
+            <xs:enumeration value="Full"/>
+          </xs:restriction>
+        </xs:simpleType>
+      </xs:attribute>
+
+      <xs:attribute name="wuDefaultAccess" use="required" default="Full">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Default access level for workunit scopes; default value is 'Full'</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+        <xs:simpleType>
+          <xs:restriction base="xs:string">
+            <xs:enumeration value="None"/>
+            <xs:enumeration value="Full"/>
+          </xs:restriction>
+        </xs:simpleType>
+      </xs:attribute>
+
+      <xs:attribute name="fileDefaultAccess" use="required" default="Full">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Default access level for file scopes; default value is 'Full'</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+        <xs:simpleType>
+          <xs:restriction base="xs:string">
+            <xs:enumeration value="None"/>
+            <xs:enumeration value="Full"/>
+          </xs:restriction>
+        </xs:simpleType>
+      </xs:attribute>
+
+      <!-- All SecurityManager Plugins must define the type="SecurityManager" attribute -->
+      <xs:attribute name="type" type="SecurityManager" use="required" default="SecurityManager">
+        <xs:annotation>
+          <xs:appinfo>
+            <viewType>hidden</viewType>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <!-- All SecurityManager Plugins must define the libName attribute -->
+      <xs:attribute name="libName" type="xs:string" use="optional" default="libjwtSecurity.so">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>The Security Manager library name (.so) and optional path</tooltip>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <!-- All SecurityManager Plugins must define the instanceFactoryName attribute -->
+      <xs:attribute name="instanceFactoryName" type="xs:string" use="optional" default="createInstance">
+        <xs:annotation>
+          <xs:appinfo>
+            <viewType>hidden</viewType>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+      <!-- All SecurityManager Plugins must define the (instance) name attribute -->
+      <xs:attribute name="name" type="xs:string" use="required">
+        <xs:annotation>
+          <xs:appinfo>
+            <tooltip>Name for this JWT Security Manager instance</tooltip>
+            <required>true</required>
+          </xs:appinfo>
+        </xs:annotation>
+      </xs:attribute>
+
+    </xs:complexType>
+  </xs:element>
+</xs:schema>

+ 1 - 0
system/security/plugins/jwtSecurity/jwt-cpp

@@ -0,0 +1 @@
+Subproject commit 49d29e3d43df817285f3262a0f4e4991732c6daa

+ 397 - 0
system/security/plugins/jwtSecurity/jwtCache.cpp

@@ -0,0 +1,397 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2020 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#include <chrono>
+#include <fnmatch.h>
+#include <iostream>
+#include <regex>
+
+#include "jlog.hpp"
+
+#include "jwtCache.hpp"
+
+JWTUserInfo::JWTUserInfo()
+    :   expireTimeUTC(0)
+{
+}
+
+JWTUserInfo::~JWTUserInfo()
+{
+}
+
+bool JWTUserInfo::isValid() const
+{
+    return expireTimeUTC > 0;
+}
+
+time_t JWTUserInfo::getExpirationTime() const
+{
+    return expireTimeUTC;
+}
+
+JWTUserInfo& JWTUserInfo::setExpirationTime(time_t newExpTime)
+{
+    expireTimeUTC = newExpTime;
+
+    return *this;
+}
+
+bool JWTUserInfo::isExpired() const
+{
+    time_t  timeNow = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+
+    return isValid() && std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) > expireTimeUTC;
+}
+
+std::string JWTUserInfo::getRefreshToken() const
+{
+    return refreshToken;
+}
+
+JWTUserInfo& JWTUserInfo::setRefreshToken(const std::string& newRefreshToken)
+{
+    refreshToken = newRefreshToken;
+
+    return *this;
+}
+
+std::string JWTUserInfo::getJWTToken() const
+{
+    return jwtToken;
+}
+
+JWTUserInfo& JWTUserInfo::setJWTToken(const std::string& newJWTToken)
+{
+    jwtToken = newJWTToken;
+
+    return *this;
+}
+
+//-----------------------------------------------------------------------------
+
+bool JWTUserInfo::hasFeaturePerm(const std::string& permName) const
+{
+    return featurePermMap.find(permName) != featurePermMap.end();
+}
+
+JWTUserInfo& JWTUserInfo::setFeaturePerm(const std::string& permName, SecAccessFlags accessFlag)
+{
+    featurePermMap[permName] = accessFlag;
+
+    return *this;
+}
+
+JWTUserInfo& JWTUserInfo::setFeaturePerm(const std::string& permName, const std::string& accessFlagName)
+{
+    return setFeaturePerm(permName, getSecAccessFlagValue(accessFlagName.c_str()));
+}
+
+JWTUserInfo& JWTUserInfo::mergeFeaturePerm(const std::string& permName, SecAccessFlags accessFlag)
+{
+    PermissionMap::const_iterator   foundIter = featurePermMap.find(permName);
+
+    if (foundIter != featurePermMap.end())
+        featurePermMap[permName] = static_cast<SecAccessFlags>(foundIter->second | accessFlag);
+    else
+        featurePermMap[permName] = accessFlag;
+
+    return *this;
+}
+
+JWTUserInfo& JWTUserInfo::mergeFeaturePerm(const std::string& permName, const std::string& accessFlagName)
+{
+    return mergeFeaturePerm(permName, getSecAccessFlagValue(accessFlagName.c_str()));
+}
+
+SecAccessFlags JWTUserInfo::getFeaturePerm(const std::string& permName, SecAccessFlags defaultFlag) const
+{
+    SecAccessFlags                  accessValue = defaultFlag;
+    PermissionMap::const_iterator   foundIter = featurePermMap.find(permName);
+
+    if (foundIter != featurePermMap.end())
+        accessValue = foundIter->second;
+
+    return accessValue;
+}
+
+JWTUserInfo& JWTUserInfo::eraseFeaturePerm(const std::string& permName)
+{
+    featurePermMap.erase(permName);
+
+    return *this;
+}
+
+JWTUserInfo& JWTUserInfo::clearFeaturePerms()
+{
+    featurePermMap.clear();
+
+    return *this;
+}
+
+unsigned int JWTUserInfo::countFeaturePerms() const
+{
+    return featurePermMap.size();
+}
+
+//-----------------------------------------------------------------------------
+
+JWTUserInfo& JWTUserInfo::addWUScopePerm(const std::string& scope, SecAccessFlags accessFlag, SecAccessFlags defaultFlag, bool asDeny)
+{
+    return _addScopePerm(wuScopePermMap, scope, accessFlag, defaultFlag, asDeny);
+}
+
+JWTUserInfo& JWTUserInfo::addWUScopePerm(const std::string& scope, const std::string& accessFlagName, SecAccessFlags defaultFlag, bool asDeny)
+{
+    return _addScopePerm(wuScopePermMap, scope, getSecAccessFlagValue(accessFlagName.c_str()), defaultFlag, asDeny);
+}
+
+SecAccessFlags JWTUserInfo::matchWUScopePerm(const std::string& scope, SecAccessFlags defaultFlag) const
+{
+    return _matchScopePerm(wuScopePermMap, scope, defaultFlag);
+}
+
+JWTUserInfo& JWTUserInfo::eraseWUScopePerm(const std::string& scope)
+{
+    return _eraseScopePerm(wuScopePermMap, scope);
+}
+
+JWTUserInfo& JWTUserInfo::clearWUScopePerm()
+{
+    return _clearScopePerm(wuScopePermMap);
+}
+
+unsigned int JWTUserInfo::countWUScopePerms() const
+{
+    return _countScopePerms(wuScopePermMap);
+}
+
+bool JWTUserInfo::hasWUScopePerms() const
+{
+    return !wuScopePermMap.empty();
+}
+
+//-----------------------------------------------------------------------------
+
+JWTUserInfo& JWTUserInfo::addFileScopePerm(const std::string& scope, SecAccessFlags accessFlag, SecAccessFlags defaultFlag, bool asDeny)
+{
+    return _addScopePerm(fileScopePermMap, scope, accessFlag, defaultFlag, asDeny);
+}
+
+JWTUserInfo& JWTUserInfo::addFileScopePerm(const std::string& scope, const std::string& accessFlagName, SecAccessFlags defaultFlag, bool asDeny)
+{
+    return _addScopePerm(fileScopePermMap, scope, getSecAccessFlagValue(accessFlagName.c_str()), defaultFlag, asDeny);
+}
+
+SecAccessFlags JWTUserInfo::matchFileScopePerm(const std::string& scope, SecAccessFlags defaultFlag) const
+{
+    return _matchScopePerm(fileScopePermMap, scope, defaultFlag);
+}
+
+JWTUserInfo& JWTUserInfo::eraseFileScopePerm(const std::string& scope)
+{
+    return _eraseScopePerm(fileScopePermMap, scope);
+}
+
+JWTUserInfo& JWTUserInfo::clearFileScopePerm()
+{
+    return _clearScopePerm(fileScopePermMap);
+}
+
+unsigned int JWTUserInfo::countFileScopePerms() const
+{
+    return _countScopePerms(fileScopePermMap);
+}
+
+bool JWTUserInfo::hasFileScopePerms() const
+{
+    return !fileScopePermMap.empty();
+}
+
+//-----------------------------------------------------------------------------
+// Private methods
+//-----------------------------------------------------------------------------
+
+// regex patterns for converting a filename glob pattern
+// to an example string
+static const std::regex _globRegex1("\\[![0-9].+?\\]\\*?");                 // foo[!1] -> fooa, foo[!1]* -> fooa
+static const std::regex _globRegex2("\\[([0-9]|![A-Za-z]).+?\\]\\*?");      // foo[1] -> foo1, foo[1]* -> foo1, foo[!m] -> foo1, foo[!m]* -> foo1
+static const std::regex _globRegex3("\\[[A-Za-z].+?\\]\\*?");               // foo[m] -> fooa, foo[m]* -> fooa
+static const std::regex _globRegex4("[*?]");                                // foo* -> fooa, foo? -> fooa
+
+std::string JWTUserInfo::_globToExample(const std::string& pattern) const
+{
+    std::string s1 = std::regex_replace(pattern, _globRegex1, "a");
+    std::string s2 = std::regex_replace(s1, _globRegex2, "1");
+    std::string s3 = std::regex_replace(s2, _globRegex3, "a");
+    std::string s4 = std::regex_replace(s3, _globRegex4, "a");
+
+    return s4;
+}
+
+std::string JWTUserInfo::_convertPathname(const std::string& logicalPath) const
+{
+    std::string newPath(std::regex_replace(logicalPath, std::regex("::"), "/"));
+
+    if (!newPath.empty())
+    {
+        if (newPath.back() == '/')
+            newPath += "*";
+        else if (newPath.back() != '*')
+            newPath += "/*";
+    }
+
+    return newPath;
+}
+
+JWTUserInfo& JWTUserInfo::_addScopePerm(PermissionMap& scopePermMap, const std::string& scope, SecAccessFlags accessFlag, SecAccessFlags defaultFlag, bool asDeny)
+{
+    std::string                 rewrittenScope(_convertPathname(scope));
+    PermissionMap::iterator     foundIter = scopePermMap.find(rewrittenScope);
+
+    if (foundIter != scopePermMap.end())
+    {
+        // Exact match on scope; apply access to existing bitmask
+        if (asDeny)
+            foundIter->second = static_cast<SecAccessFlags>(foundIter->second & ~accessFlag);
+        else
+            foundIter->second = static_cast<SecAccessFlags>(foundIter->second | accessFlag);
+    }
+    else
+    {
+        // Find most-applicable scope access value and apply new access to that bitmask
+        SecAccessFlags  existingFlags = _matchScopePerm(scopePermMap, scope, defaultFlag);
+
+        if (asDeny)
+            scopePermMap[rewrittenScope] = static_cast<SecAccessFlags>(existingFlags & ~accessFlag);
+        else
+            scopePermMap[rewrittenScope] = static_cast<SecAccessFlags>(existingFlags | accessFlag);
+    }
+
+    return *this;
+}
+
+JWTUserInfo& JWTUserInfo::_addScopePerm(PermissionMap& scopePermMap, const std::string& scope, const std::string& accessFlagName, SecAccessFlags defaultFlag, bool asDeny)
+{
+    return _addScopePerm(scopePermMap, scope, getSecAccessFlagValue(accessFlagName.c_str()), defaultFlag, asDeny);
+}
+
+SecAccessFlags JWTUserInfo::_matchScopePerm(const PermissionMap& scopePermMap, const std::string& scope, SecAccessFlags defaultFlag) const
+{
+    // Ideally, this function should be traversing a tree of scope permissions, with each
+    // scope level a tree node; that would be much faster and would also avoid requiring
+    // ordering multiple patterns prior to calling _addScopePerm()
+    std::string         scopeAsExample(_convertPathname(_globToExample(scope)));
+    SecAccessFlags      accessValue = defaultFlag;
+    size_t              longestScopeLen = 0;
+
+    for (PermissionMap::const_iterator x = scopePermMap.begin(); x != scopePermMap.end(); x++)
+    {
+        if (::fnmatch(x->first.c_str(), scopeAsExample.c_str(), (FNM_CASEFOLD | FNM_LEADING_DIR)) == 0)
+        {
+            // Use only the longest (most precise) scope
+            if (x->first.size() > longestScopeLen)
+            {
+                longestScopeLen = x->first.size();
+                accessValue = x->second;
+            }
+        }
+    }
+
+    return accessValue;
+}
+
+JWTUserInfo& JWTUserInfo::_eraseScopePerm(PermissionMap& scopePermMap, const std::string& scope)
+{
+    std::string rewrittenScope(_convertPathname(scope));
+
+    scopePermMap.erase(rewrittenScope);
+
+    return *this;
+}
+
+JWTUserInfo& JWTUserInfo::_clearScopePerm(PermissionMap& scopePermMap)
+{
+    scopePermMap.clear();
+
+    return *this;
+}
+
+unsigned int JWTUserInfo::_countScopePerms(const PermissionMap& scopePermMap) const
+{
+    return scopePermMap.size();
+}
+
+//=============================================================================
+
+bool JWTUserCache::has(const std::string& userName) const
+{
+    CriticalBlock block(crit);
+    return userPermMap.find(userName) != userPermMap.end();
+}
+
+JWTUserCache& JWTUserCache::set(const std::string& userName, std::shared_ptr<JWTUserInfo>& userInfo)
+{
+    {
+        CriticalBlock block(crit);
+
+        userPermMap[userName] = userInfo;
+    }
+
+    return *this;
+}
+
+const std::shared_ptr<JWTUserInfo> JWTUserCache::get(const std::string& userName) const
+{
+    {
+        CriticalBlock                       block(crit);
+        UserPermissionMap::const_iterator   foundIter = userPermMap.find(userName);
+
+        if (foundIter != userPermMap.end())
+        {
+            return foundIter->second;
+        }
+    }
+
+    return nullptr;
+}
+
+JWTUserCache& JWTUserCache::erase(const std::string& userName)
+{
+    {
+        CriticalBlock block(crit);
+
+        userPermMap.erase(userName);
+    }
+
+    return *this;
+}
+
+JWTUserCache& JWTUserCache::clear()
+{
+    {
+        CriticalBlock block(crit);
+
+        userPermMap.clear();
+    }
+
+    return *this;
+}
+
+unsigned int JWTUserCache::count() const
+{
+    return userPermMap.size();
+}

+ 455 - 0
system/security/plugins/jwtSecurity/jwtCache.hpp

@@ -0,0 +1,455 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2020 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef JWTCACHE_HPP_
+#define JWTCACHE_HPP_
+
+#include <map>
+#include <string>
+
+#include "basesecurity.hpp"
+
+/**
+ * Instances of JWTUserInfo represent all of the cached values for a particular
+ * user.  The intention is that instances would be accessed via another
+ * container, such as a map keyed off the user's username.  See JWTUserCache class.
+ */
+class JWTUserInfo
+{
+    private:
+
+        typedef std::map<std::string, SecAccessFlags> PermissionMap;
+
+    public:
+
+        JWTUserInfo();
+
+        ~JWTUserInfo();
+
+        /**
+         * @return  true if the object is valid, false otherwise
+         */
+        bool isValid() const;
+
+        /**
+         * @return  The expiration time for the user info as the number of
+         *          seconds after epoch in UTC.
+         *
+         * @see     isExpired
+         * @see     setExpirationTime
+         */
+        time_t getExpirationTime() const;
+
+        /**
+         * Sets the expiration tme for the user info.
+         *
+         * @param   newExpTime      Expiration time as the number of
+         *                          seconds after epoch in UTC.
+         *
+         * @return  Reference to update object (this).
+         *
+         * @see     getExpirationTime
+         * @see     isExpired
+         */
+        JWTUserInfo& setExpirationTime(time_t newExpTime);
+
+        /**
+         * @return  true if the save expiration is prior to the current time,
+         *          false otherwise.
+         *
+         * @see     getExpirationTime
+         * @see     setExpirationTime
+         */
+        bool isExpired() const;
+
+        /**
+         * @return  Copy of the saved refresh token.
+         *
+         * @see     setRefreshToken
+         */
+        std::string getRefreshToken() const;
+
+        /**
+         * Saves a new refresh token in the object.
+         *
+         * @param   newRefreshToken     The token to save
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     getRefreshToken
+         */
+        JWTUserInfo& setRefreshToken(const std::string& newRefreshToken);
+
+        /**
+         * @return  Copy of the saved authentication token.  This
+         *          could be used to authenticate the associated
+         *          user with another JWT-aware service.
+         *
+         * @see     setJWTToken
+         */
+        std::string getJWTToken() const;
+
+        /**
+         * Saves a new authentication token in the object.
+         *
+         * @param   newJWTToken     The token to save
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     getJWTToken
+         */
+        JWTUserInfo& setJWTToken(const std::string& newJWTToken);
+
+        /**
+         * @param   permName        A permission name/key
+         *
+         * @return  true if there is any kind of entry for permName in the
+         *          object's table, false otherwise
+         *
+         * @see     getFeaturePerm
+         * @see     mergeFeaturePerm
+         * @see     setFeaturePerm
+         */
+        bool hasFeaturePerm(const std::string& permName) const;
+
+        /**
+         * Set the value for a feature permission, overwriting an
+         * existing value, if any.
+         *
+         * @param   permName        A permission name/key
+         * @param   accessFlag      The flag value to save
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     getFeaturePerm
+         * @see     hasFeaturePerm
+         * @see     mergeFeaturePerm
+         */
+        JWTUserInfo& setFeaturePerm(const std::string& permName, SecAccessFlags accessFlag);
+
+        /**
+         * Set the value for a feature permission, overwriting an
+         * existing value, if any.
+         *
+         * @param   permName        A permission name/key
+         * @param   accessFlagName  The name of a flag value to save; must be one of
+         *                          [None, Access, Read, Write, Full]; case-sensitive
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     getFeaturePerm
+         * @see     hasFeaturePerm
+         * @see     mergeFeaturePerm
+         */
+        JWTUserInfo& setFeaturePerm(const std::string& permName, const std::string& accessFlagName);
+
+        /**
+         * Modify the value for a feature permission.  If a permission did not exist
+         * previously then the permission takes on the given value; if the permission
+         * did exist then the new valuee is OR'd with the old value.
+         *
+         * @param   permName        A permission name/key
+         * @param   accessFlag      The flag value to save
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     getFeaturePerm
+         * @see     hasFeaturePerm
+         * @see     setFeaturePerm
+         */
+        JWTUserInfo& mergeFeaturePerm(const std::string& permName, SecAccessFlags accessFlag);
+
+        /**
+         * Modify the value for a feature permission.  If a permission did not exist
+         * previously then the permission takes on the given value; if the permission
+         * did exist then the new valuee is OR'd with the old value.
+         *
+         * @param   permName        A permission name/key
+         * @param   accessFlagName  The name of a flag value to save; must be one of
+         *                          [None, Access, Read, Write, Full]; case-sensitive
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     getFeaturePerm
+         * @see     hasFeaturePerm
+         * @see     setFeaturePerm
+         */
+        JWTUserInfo& mergeFeaturePerm(const std::string& permName, const std::string& accessFlagName);
+
+        /**
+         * Returns the value for a feature permission if one is found, or the given default
+         * value if not found.
+         *
+         * @param   permName        A permission name/key
+         * @param   defaultFlag     The value to return if permName is not found in the object
+         *
+         * @return  A previously-saved permission value or the value of defaultFlag if the
+         *          permission had not been saved.
+         *
+         * @see     hasFeaturePerm
+         * @see     mergeFeaturePerm
+         * @see     setFeaturePerm
+         */
+        SecAccessFlags getFeaturePerm(const std::string& permName, SecAccessFlags defaultFlag) const;
+
+        /**
+         * Erase a permission value from the object.  Does nothing when the given permission
+         * does not exist.
+         *
+         * @param   permName        A permission name/key
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     clearFeaturePerms
+         */
+        JWTUserInfo& eraseFeaturePerm(const std::string& permName);
+
+        /**
+         * Erase all permission values from the object.
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     eraseFeaturePerm
+         */
+        JWTUserInfo& clearFeaturePerms();
+
+        /**
+         * @return  The number of permissions currently saved.
+         */
+        unsigned int countFeaturePerms() const;
+
+        /**
+         * Merge the value for a workunit scope permission with the most-applicable
+         * workunit scope (if any) and saves it.
+         *
+         * @param   scope           A workunit scope pattern
+         * @param   accessFlag      The flag value to save
+         * @param   defaultFlag     The default flag value; used only when asDeny
+         *                          is true and no previous scope flag is found
+         * @param   asDeny          If true, treat accessFlag as a denial of
+         *                          that permission; if false, treat accessFlag
+         *                          as a grant of that permission; defaults to
+         *                          false
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     matchWUScopePerm
+         */
+        JWTUserInfo& addWUScopePerm(const std::string& scope, SecAccessFlags accessFlag, SecAccessFlags defaultFlag, bool asDeny = false);
+
+        /**
+         * Merge the value for a workunit scope permission with the most-applicable
+         * workunit scope (if any) and saves it.
+         *
+         * @param   scope           A workunit scope pattern
+         * @param   accessFlagName  The name of a flag value to save; must be one of
+         *                          [None, Access, Read, Write, Full]; case-sensitive
+         * @param   defaultFlag     The default flag value; used only when asDeny
+         *                          is true and no previous scope flag is found
+         * @param   asDeny          If true, treat accessFlagName as a denial of
+         *                          that permission; if false, treat accessFlagName
+         *                          as a grant of that permission; defaults to
+         *                          false
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     matchWUScopePerm
+         */
+        JWTUserInfo& addWUScopePerm(const std::string& scope, const std::string& accessFlagName, SecAccessFlags defaultFlag, bool asDeny = false);
+
+        /**
+         * Returns the value for a workunit scope permission if one is found or the given default
+         * value if not found.  A match is determined using the same mechanism as is used for
+         * filename globbing in Linux.  In the event of multiple matches, the longest match wins.
+         *
+         * @param   scope           A workunit scope
+         * @param   defaultFlag     The value to return if scope is not found in the object
+         *
+         * @return  A previously-saved permission value or the value of defaultFlag if a matching
+         *          scope cannot be found.
+         *
+         * @see     addWUScopePerm
+         */
+        SecAccessFlags matchWUScopePerm(const std::string& scope, SecAccessFlags defaultFlag) const;
+
+        /**
+         * Erase a workunit scope value from the object.  Does nothing when the given scope
+         * does not exist.
+         *
+         * @param   scope           A workunit scope pattern
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     clearWUScopePerm
+         */
+        JWTUserInfo& eraseWUScopePerm(const std::string& scope);
+
+        /**
+         * Erase all workunit scope values from the object.
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     eraseWUScopePerm
+         */
+        JWTUserInfo& clearWUScopePerm();
+
+        /**
+         * @return  The number of workunit scopes currently saved.
+         */
+        unsigned int countWUScopePerms() const;
+
+        /**
+         * @return  True if any workunit scopes have been set, false otherwise.
+         */
+        bool hasWUScopePerms() const;
+
+        /**
+         * Merge the value for a file scope permission with the most-applicable
+         * file scope (if any) and saves it.
+         *
+         * @param   scope           A file scope pattern
+         * @param   accessFlag      The flag value to save
+         * @param   defaultFlag     The default flag value; used only when asDeny
+         *                          is true and no previous scope flag is found
+         * @param   asDeny          If true, treat accessFlag as a denial of
+         *                          that permission; if false, treat accessFlag
+         *                          as a grant of that permission; defaults to
+         *                          false
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     matchFileScopePerm
+         */
+        JWTUserInfo& addFileScopePerm(const std::string& scope, SecAccessFlags accessFlag, SecAccessFlags defaultFlag, bool asDeny = false);
+
+        /**
+         * Merge the value for a fil scope permission with the most-applicable
+         * file scope (if any) and saves it.
+         *
+         * @param   scope           A file scope pattern
+         * @param   accessFlagName  The name of a flag value to save; must be one of
+         *                          [None, Access, Read, Write, Full]; case-sensitive
+         * @param   defaultFlag     The default flag value; used only when asDeny
+         *                          is true and no previous scope flag is found
+         * @param   asDeny          If true, treat accessFlagName as a denial of
+         *                          that permission; if false, treat accessFlagName
+         *                          as a grant of that permission; defaults to
+         *                          false
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     matchFileScopePerm
+         */
+        JWTUserInfo& addFileScopePerm(const std::string& scope, const std::string& accessFlagName, SecAccessFlags defaultFlag, bool asDeny = false);
+
+        /**
+         * Returns the value for a file scope permission if one is found or the given default
+         * value if not found.  A match is determined using the same mechanism as is used for
+         * filename globbing in Linux.  In the event of multiple matches, the longest match wins.
+         *
+         * @param   scope           A file scope
+         * @param   defaultFlag     The value to return if scope is not found in the object
+         *
+         * @return  A previously-saved permission value or the value of defaultFlag if a matching
+         *          scope cannot be found.
+         *
+         * @see     addFileScopePerm
+         */
+        SecAccessFlags matchFileScopePerm(const std::string& scope, SecAccessFlags defaultFlag) const;
+
+        /**
+         * Erase a file scope value from the object.  Does nothing when the given scope
+         * does not exist.
+         *
+         * @param   scope           A file scope pattern
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     clearFileScopePerm
+         */
+        JWTUserInfo& eraseFileScopePerm(const std::string& scope);
+
+        /**
+         * Erase all file scope values from the object.
+         *
+         * @return  Reference to updated object (this).
+         *
+         * @see     eraseFileScopePerm
+         */
+        JWTUserInfo& clearFileScopePerm();
+
+        /**
+         * @return  The number of file scopes currently saved.
+         */
+        unsigned int countFileScopePerms() const;
+
+        /**
+         * @return  True if any file scopes have been set, false otherwise.
+         */
+        bool hasFileScopePerms() const;
+
+    private:
+
+        std::string _globToExample(const std::string& pattern) const;
+        std::string _convertPathname(const std::string& logicalPath) const;
+        JWTUserInfo& _addScopePerm(PermissionMap& scopePermMap, const std::string& scope, SecAccessFlags accessFlag, SecAccessFlags defaultFlag, bool asDeny);
+        JWTUserInfo& _addScopePerm(PermissionMap& scopePermMap, const std::string& scope, const std::string& accessFlagName, SecAccessFlags defaultFlag, bool asDeny);
+        SecAccessFlags _matchScopePerm(const PermissionMap& scopePermMap, const std::string& scope, SecAccessFlags defaultFlag) const;
+        JWTUserInfo& _eraseScopePerm(PermissionMap& scopePermMap, const std::string& scope);
+        JWTUserInfo& _clearScopePerm(PermissionMap& scopePermMap);
+        unsigned int _countScopePerms(const PermissionMap& scopePermMap) const;
+
+    private:
+
+        time_t                  expireTimeUTC;      //!< Time this object expires
+        std::string             refreshToken;       //!< The refresh token associated with jwtToken
+        std::string             jwtToken;           //!< Raw token provided during authentication; suitable for passing to another service
+        PermissionMap           featurePermMap;     //!< Map of FeatureName -> PermissionFlag
+        PermissionMap           wuScopePermMap;     //!< Map of ScopePattern -> PermissionFlag
+        PermissionMap           fileScopePermMap;   //!< Map of ScopePattern -> PermissionFlag
+};
+
+/**
+ * This is basically a thread-safe map of username->JWTUserInfo objects.
+ */
+class JWTUserCache
+{
+    private:
+
+        typedef std::map<std::string, std::shared_ptr<JWTUserInfo> > UserPermissionMap;
+
+    public:
+
+        bool has(const std::string& userName) const;
+
+        JWTUserCache& set(const std::string& userName, std::shared_ptr<JWTUserInfo>& userInfo);
+
+        const std::shared_ptr<JWTUserInfo> get(const std::string& userName) const;
+
+        JWTUserCache& erase(const std::string& userName);
+
+        JWTUserCache& clear();
+
+        unsigned int count() const;
+
+    private:
+
+        mutable CriticalSection     crit;           //!< Used to prevent thread collisions during userPermMap modification
+        UserPermissionMap           userPermMap;    //!< Map of UserName -> Permissions
+};
+
+#endif // JWTCACHE_HPP_

+ 187 - 0
system/security/plugins/jwtSecurity/jwtEndpoint.cpp

@@ -0,0 +1,187 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2020 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#include <curl/curl.h>
+#include <iostream>
+#include <openssl/sha.h>
+#include <string.h>
+
+#include "jlog.hpp"
+
+#include "nlohmann/json.hpp"
+
+#include "jwtEndpoint.hpp"
+
+static size_t captureIncomingCURLReply(void* contents, size_t size, size_t nmemb, void* userp)
+{
+    size_t          incomingDataSize = size * nmemb;
+    MemoryBuffer*   mem = static_cast<MemoryBuffer*>(userp);
+    size_t          MAX_BUFFER_SIZE = 4194304; // 2^22
+
+    if ((mem->length() + incomingDataSize) < MAX_BUFFER_SIZE)
+    {
+        mem->append(incomingDataSize, contents);
+    }
+    else
+    {
+        // Signals an error to libcurl
+        incomingDataSize = 0;
+
+        PROGLOG("CJwtSecurityManager::captureIncomingCURLReply() exceeded buffer size %zu", MAX_BUFFER_SIZE);
+    }
+
+    return incomingDataSize;
+}
+
+static std::string hashString(const std::string& s)
+{
+    SHA256_CTX  context;
+    char        hashedValue[SHA256_DIGEST_LENGTH + 1];
+
+    memset(hashedValue, 0, sizeof(hashedValue));
+
+    if (!SHA256_Init(&context))
+        throw makeStringException(-1, "CJwtSecurityManager: OpenSSL ERROR calling SHA256_Init while hashing user password");
+
+    if (!SHA256_Update(&context, (unsigned char*)s.c_str(), s.size()))
+        throw makeStringException(-1, "CJwtSecurityManager: OpenSSL ERROR calling SHA256_Update while hashing user password");
+
+    if (!SHA256_Final((unsigned char*)hashedValue, &context))
+        throw makeStringException(-1, "CJwtSecurityManager: OpenSSL ERROR calling SHA256_Final while hashing user password");
+
+    return hashedValue;
+}
+
+static std::string hashUserPW(const std::string& pw, const std::string& nonce)
+{
+    std::string     firstHash(hashString(pw));
+    std::string     secondHash(hashString(firstHash + nonce));
+
+    return secondHash;
+}
+
+static std::string tokenFromEndpoint(const std::string& jwtEndPoint, bool allowSelfSignedCert, const std::string& credentialsStr)
+{
+    std::string     apiResponse;
+
+    // Call JWT endpoint
+    CURL*   curlHandle = curl_easy_init();
+    if (curlHandle)
+    {
+        CURLcode                curlResponseCode;
+        struct curl_slist*      headers = nullptr;
+        size_t                  INITIAL_BUFFER_SIZE = 32768; // 2^15
+        MemoryBuffer            captureBuffer(INITIAL_BUFFER_SIZE);
+        char                    curlErrBuffer[CURL_ERROR_SIZE];
+
+        curlErrBuffer[0] = '\0';
+
+        try
+        {
+            headers = curl_slist_append(headers, "Content-Type: application/json;charset=UTF-8");
+            curl_easy_setopt(curlHandle, CURLOPT_HTTPHEADER, headers);
+
+            curl_easy_setopt(curlHandle, CURLOPT_URL, jwtEndPoint.c_str());
+            curl_easy_setopt(curlHandle, CURLOPT_POST, 1);
+            curl_easy_setopt(curlHandle, CURLOPT_POSTFIELDS, credentialsStr.c_str());
+            if (allowSelfSignedCert)
+                curl_easy_setopt(curlHandle, CURLOPT_SSL_VERIFYPEER, 0);
+            curl_easy_setopt(curlHandle, CURLOPT_FOLLOWLOCATION, 1);
+            curl_easy_setopt(curlHandle, CURLOPT_NOPROGRESS, 1);
+            curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, captureIncomingCURLReply);
+            curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, static_cast<void*>(&captureBuffer));
+            curl_easy_setopt(curlHandle, CURLOPT_ERRORBUFFER, curlErrBuffer);
+
+            curlResponseCode = curl_easy_perform(curlHandle);
+
+            if (curlResponseCode == CURLE_OK && captureBuffer.length() > 0)
+            {
+                std::string     responseStr = std::string(captureBuffer.toByteArray(), captureBuffer.length());
+
+                // Poor check to see if reply resembles JSON
+                if (responseStr[0] == '{')
+                    apiResponse = responseStr;
+                else
+                    PROGLOG("CJwtSecurityManager: Invalid JWT endpoint response: %s", responseStr.c_str());
+            }
+            else
+            {
+                if (curlResponseCode != CURLE_OK)
+                    PROGLOG("CJwtSecurityManager login error: libcurl error (%d): %s", curlResponseCode, (curlErrBuffer[0] ? curlErrBuffer : "<unknown>"));
+                else
+                    PROGLOG("CJwtSecurityManager login error: No content from JWT endpoint");
+            }
+        }
+        catch (...)
+        {
+            // We should not be allowing exceptions to propagate up
+            // the chain; ignore the error (and let's hope the source
+            // of the error logged the details)
+        }
+
+        // Cleanup
+        if (headers)
+        {
+            curl_slist_free_all(headers);
+            headers = nullptr;
+        }
+        curl_easy_cleanup(curlHandle);
+    }
+
+    return apiResponse;
+}
+
+std::string tokenFromLogin(const std::string& jwtEndPoint, bool allowSelfSignedCert, const std::string& userStr, const std::string& pwStr, const std::string& clientID, const std::string& nonce)
+{
+    nlohmann::json          credentialsJSON;
+    std::string             credentialsStr;
+
+    // Construct the credentials
+    credentialsJSON["username"] = userStr;
+    credentialsJSON["password"] = hashUserPW(pwStr, nonce);
+    credentialsJSON["client_id"] = clientID;
+    credentialsJSON["nonce"] = nonce;
+    credentialsStr = credentialsJSON.dump();
+
+    return tokenFromEndpoint(jwtEndPoint, allowSelfSignedCert, credentialsStr);
+}
+
+std::string tokenFromRefresh(const std::string& jwtEndPoint, bool allowSelfSignedCert, const std::string& clientID, const std::string& refreshToken)
+{
+    nlohmann::json          credentialsJSON;
+    std::string             credentialsStr;
+
+    // Construct the credentials
+    credentialsJSON["grant_type"] = "refresh_token";
+    credentialsJSON["refresh_token"] = refreshToken;
+    credentialsJSON["client_id"] = clientID;
+    credentialsStr = credentialsJSON.dump();
+
+    return tokenFromEndpoint(jwtEndPoint, allowSelfSignedCert, credentialsStr);
+}
+
+MODULE_INIT(INIT_PRIORITY_STANDARD)
+{
+    curl_global_init(CURL_GLOBAL_ALL);
+
+    return true;
+}
+
+MODULE_EXIT()
+{
+    curl_global_cleanup();
+}

+ 61 - 0
system/security/plugins/jwtSecurity/jwtEndpoint.hpp

@@ -0,0 +1,61 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2020 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef JWTENDPOINT_HPP_
+#define JWTENDPOINT_HPP_
+
+#include <string>
+
+/**
+ * Calls the JWT login endpoint, submitting user credentials.  Expects a
+ * JSON-formatted response.
+ *
+ * @param   jwtLoginEndpoint        Full URL to JWT login endpoint
+ * @param   allowSelfSignedCert     If true, allow a self-signed certificate
+ *                                  to validate https conneection; ignored
+ *                                  for http connections, but you shouldn't
+ *                                  be using those anyway
+ * @param   userStr                 Username to validate
+ * @param   pwStr                   Password to validate
+ * @param   clientID                The client_id to pass to the endpoint
+ * @param   nonce                   Nonce to pass to the endpoint
+ *
+ * @return  String containing a JSON response from the endpoint.  If an
+ *          error occurs that results in something other than a JSON
+ *          string then an empty string will be returned.
+ */
+std::string tokenFromLogin(const std::string& jwtLoginEndpoint, bool allowSelfSignedCert, const std::string& userStr, const std::string& pwStr, const std::string& clientID, const std::string& nonce);
+
+/**
+ * Calls the JWT refress endpoint, submitting a refresh token.  Expects a
+ * JSON-formatted response.
+ *
+ * @param   jwtEndPoint             Full URL to JWT refresh endpoint
+ * @param   allowSelfSignedCert     If true, allow a self-signed certificate
+ *                                  to validate https conneection; ignored
+ *                                  for http connections, but you shouldn't
+ *                                  be using those anyway
+ * @param   clientID                The client_id to pass to the endpoint
+ * @param   refreshToken            Refresh token to pass to the endpoint
+ *
+ * @return  String containing a JSON response from the endpoint.  If an
+ *          error occurs that results in something other than a JSON
+ *          string then an empty string will be returned.
+ */
+std::string tokenFromRefresh(const std::string& jwtEndPoint, bool allowSelfSignedCert, const std::string& clientID, const std::string& refreshToken);
+
+#endif // JWTENDPOINT_HPP_

File diff suppressed because it is too large
+ 1148 - 0
system/security/plugins/jwtSecurity/jwtSecurity.cpp


+ 36 - 0
system/security/plugins/jwtSecurity/jwtSecurity.hpp

@@ -0,0 +1,36 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2020 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef JWTSECURITY_HPP_
+#define JWTSECURITY_HPP_
+
+#ifndef JWTSECURITY_API
+
+#ifndef JWTSECURITY_EXPORTS
+    #define JWTSECURITY_API DECL_IMPORT
+#else
+    #define JWTSECURITY_API DECL_EXPORT
+#endif //JWTSECURITY_EXPORTS
+
+#endif
+
+extern "C"
+{
+    JWTSECURITY_API ISecManager* createInstance(const char* serviceName, IPropertyTree& secMgrCfg, IPropertyTree& bndCfg);
+}
+
+#endif // JWTSECURITY_HPP_

+ 2 - 1
system/security/shared/seclib.hpp

@@ -418,7 +418,8 @@ enum secManagerType : int
     SMT_LDAP,
     SMT_HTPasswd,
     SMT_SingleUser,
-    SMT_HTPluggable
+    SMT_HTPluggable,
+    SMT_JWTAuth
 };
 
 static const SecFeatureBit SMF_NO_FEATURES                    = 0x00;

+ 7 - 6
system/security/shared/secloader.hpp

@@ -40,7 +40,8 @@ public:
     ///
     /// @return an ISecManager Security Manager instance
     ///
-    static ISecManager* loadPluggableSecManager(const char * bindingName, IPropertyTree* bindingCfg, IPropertyTree* secMgrCfg)
+    template <class SECMGR>
+    static SECMGR* loadPluggableSecManager(const char * bindingName, IPropertyTree* bindingCfg, IPropertyTree* secMgrCfg)
     {
         const char * lsm = "Load Security Manager :";
 
@@ -69,15 +70,15 @@ public:
         if(pluggableSecLib == NULL)
             throw MakeStringException(-1, "%s can't load library %s for %s", lsm, libName.str(), bindingName);
 
-        //Retrieve address of exported ISecManager instance factory
+        //Retrieve address of exported SECMGR instance factory
         newPluggableSecManager_t_ xproc = NULL;
         xproc = (newPluggableSecManager_t_)GetSharedProcedure(pluggableSecLib, instFactory.str());
         if (xproc == NULL)
             throw MakeStringException(-1, "%s cannot locate procedure %s of '%s'", lsm, instFactory.str(), libName.str());
 
-        //Call ISecManager instance factory and return the new instance
+        //Call SECMGR instance factory and return the new instance
         DBGLOG("Calling '%s' in pluggable security manager '%s'", instFactory.str(), libName.str());
-        ISecManager* pPSM = xproc(bindingName, *secMgrCfg, *bindingCfg);
+        SECMGR* pPSM = dynamic_cast<SECMGR*>(xproc(bindingName, *secMgrCfg, *bindingCfg));
         if (pPSM == nullptr)
             throw MakeStringException(-1, "%s Security Manager %s failed to instantiate in call to %s", lsm, libName.str(), instFactory.str());
         return pPSM;
@@ -107,7 +108,7 @@ public:
         }
         else
             throw MakeStringException(-1, "Security model %s not supported", model_name);
-    }   
+    }
 
     static IAuthMap* loadTheDefaultAuthMap(IPropertyTree* cfg)
     {
@@ -122,7 +123,7 @@ public:
             return xproc(cfg);
         else
             throw MakeStringException(-1, "procedure newDefaultAuthMap of %s can't be loaded", LDAPSECLIB);
-    }   
+    }
 };
 
 #endif