You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by ne...@apache.org on 2022/10/19 18:36:52 UTC

[helix] branch master updated: Add support for optional identity token source in helix-front

This is an automated email from the ASF dual-hosted git repository.

nealsun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/helix.git


The following commit(s) were added to refs/heads/master by this push:
     new cfa08e481 Add support for optional identity token source in helix-front
cfa08e481 is described below

commit cfa08e481ad2b95aee0c82b0d81368faa660a0ce
Author: Micah Stubbs <mi...@gmail.com>
AuthorDate: Wed Oct 19 11:36:47 2022 -0700

    Add support for optional identity token source in helix-front
    
    During helix-front login, optionally request an identity token
    from a configurable identity token API.
---
 helix-front/package.json               |   6 +-
 helix-front/server/.gitignore          |   1 +
 helix-front/server/config.example.ts   |  44 ++++++++++++++
 helix-front/server/config.ts           |  24 --------
 helix-front/server/controllers/d.ts    |   4 ++
 helix-front/server/controllers/user.ts | 101 ++++++++++++++++++++++++++++-----
 6 files changed, 139 insertions(+), 41 deletions(-)

diff --git a/helix-front/package.json b/helix-front/package.json
index 9e9363095..cc125e5c3 100644
--- a/helix-front/package.json
+++ b/helix-front/package.json
@@ -22,7 +22,7 @@
   "scripts": {
     "ng": "ng",
     "build": "rm -rf dist && mkdir dist && ng build --aot --configuration production && tsc -p server",
-    "build:dev": "rm -rf dist && mkdir dist && ng build --aot --configuration development && tsc -p server",
+    "build:dev": "rm -rf dist && mkdir dist && yarn check:config && ng build --aot --configuration development && tsc -p server",
     "start": "concurrently -r \"ng serve\" \"tsc -w -p server\" \"nodemon dist/server/app.js\"",
     "prod": "yarn run build && node dist/server/app.js",
     "test": "ng test",
@@ -40,7 +40,9 @@
     "type:check:watch": "yarn type:check -- --watch",
     "format:check": "npx prettier --check .",
     "format": "yarn prettier --write .",
-    "prepack": "yarn build"
+    "prepack": "yarn build",
+    "copy:config": "cp server/config.example.ts server/config.ts",
+    "check:config": "test -f server/config.ts || yarn copy:config"
   },
   "browserslist": [
     "last 2 Edge versions",
diff --git a/helix-front/server/.gitignore b/helix-front/server/.gitignore
new file mode 100644
index 000000000..c3c165302
--- /dev/null
+++ b/helix-front/server/.gitignore
@@ -0,0 +1 @@
+config.ts
diff --git a/helix-front/server/config.example.ts b/helix-front/server/config.example.ts
new file mode 100644
index 000000000..f069dc4b2
--- /dev/null
+++ b/helix-front/server/config.example.ts
@@ -0,0 +1,44 @@
+export const HELIX_ENDPOINTS = {
+  helix: [
+    {
+      default: 'http://localhost:8100/admin/v2',
+    },
+  ],
+};
+export const SESSION_STORE = undefined;
+export const SSL = {
+  port: 0,
+  keyfile: '',
+  certfile: '',
+  passfile: '',
+  cafiles: [],
+};
+export const LDAP = {
+  uri: 'ldap://example.com',
+  base: 'DC=example,DC=com',
+  principalSuffix: '@example.com',
+  adminGroup: 'admin',
+};
+
+/**
+ * The url of your Identity Token API.
+ * This an API that should expect LDAP credentials
+ * and if the LDAP credentials are valid
+ * respond with a unique token of some kind.
+ */
+export const IDENTITY_TOKEN_SOURCE: string | undefined = undefined; // 'www.example.com';
+
+/**
+ * Any custom object that you would like
+ * to include in the body of the request
+ * to your custom identity source.
+ */
+export const CUSTOM_IDENTITY_TOKEN_REQUEST_BODY: any = {};
+
+/**
+ * This is the key that helix-front uses
+ * to access the token itself
+ * from the custom identity token response
+ * sent by your Identity Token API.
+ */
+export const TOKEN_RESPONSE_KEY: string = 'token';
diff --git a/helix-front/server/config.ts b/helix-front/server/config.ts
deleted file mode 100644
index cd93f6be6..000000000
--- a/helix-front/server/config.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export const HELIX_ENDPOINTS = {
-  helix: [
-    {
-      default: 'http://localhost:8100/admin/v2',
-    },
-  ],
-};
-
-export const SESSION_STORE = undefined;
-
-export const SSL = {
-  port: 0,
-  keyfile: '',
-  certfile: '',
-  passfile: '',
-  cafiles: [],
-};
-
-export const LDAP = {
-  uri: 'ldap://example.com',
-  base: 'DC=example,DC=com',
-  principalSuffix: '@example.com',
-  adminGroup: 'admin',
-};
diff --git a/helix-front/server/controllers/d.ts b/helix-front/server/controllers/d.ts
index 79334f05a..3f3c88415 100644
--- a/helix-front/server/controllers/d.ts
+++ b/helix-front/server/controllers/d.ts
@@ -5,6 +5,10 @@ export interface HelixUserRequest extends Request {
 }
 
 interface HelixSession {
+  // since this token is from a configurable
+  // identity source, the format really is
+  // `any` from helix-front's point of view.
+  identityToken: any;
   username: string;
   isAdmin: boolean;
 }
diff --git a/helix-front/server/controllers/user.ts b/helix-front/server/controllers/user.ts
index 54dd5ac49..9a317b5ff 100644
--- a/helix-front/server/controllers/user.ts
+++ b/helix-front/server/controllers/user.ts
@@ -1,7 +1,12 @@
-import { Request, Response, Router } from 'express';
+import { Response, Router } from 'express';
 import * as LdapClient from 'ldapjs';
+import * as request from 'request';
 
-import { LDAP } from '../config';
+import {
+  LDAP,
+  IDENTITY_TOKEN_SOURCE,
+  CUSTOM_IDENTITY_TOKEN_REQUEST_BODY,
+} from '../config';
 import { HelixUserRequest } from './d';
 
 export class UserCtrl {
@@ -13,8 +18,11 @@ export class UserCtrl {
   }
 
   protected authorize(req: HelixUserRequest, res: Response) {
-    // you can rewrite this function to support your own authorization logic
-    // by default, doing nothing but redirection
+    //
+    // you can rewrite this function
+    // to support your own authorization logic
+    // by default, do nothing but redirect
+    //
     if (req.query.url) {
       res.redirect(req.query.url as string);
     } else {
@@ -26,19 +34,26 @@ export class UserCtrl {
     res.json(req.session.username || 'Sign In');
   }
 
+  //
+  // Check the server-side session store,
+  // see if this helix-front ExpressJS server
+  // already knows that the current user is an admin.
+  //
   protected can(req: HelixUserRequest, res: Response) {
     try {
       return res.json(req.session.isAdmin ? true : false);
     } catch (err) {
-      // console.log('error from can', err)
+      throw new Error(
+        `Error from /can logged in admin user session status endpoint: ${err}`
+      );
       return false;
     }
   }
 
-  protected login(request: HelixUserRequest, response: Response) {
-    const credential = request.body;
+  protected login(req: HelixUserRequest, res: Response) {
+    const credential = req.body;
     if (!credential.username || !credential.password) {
-      response.status(401).json(false);
+      res.status(401).json(false);
       return;
     }
 
@@ -49,9 +64,9 @@ export class UserCtrl {
       credential.password,
       (err) => {
         if (err) {
-          response.status(401).json(false);
+          res.status(401).json(false);
         } else {
-          // login success
+          // LDAP login success
           const opts = {
             filter:
               '(&(sAMAccountName=' +
@@ -60,6 +75,9 @@ export class UserCtrl {
             scope: 'sub',
           };
 
+          req.session.username = credential.username;
+          res.set('Username', credential.username);
+
           ldap.search(LDAP.base, opts, function (err, result) {
             let isInAdminGroup = false;
             result.on('searchEntry', function (entry) {
@@ -69,14 +87,67 @@ export class UserCtrl {
                   const groupName = group.split(',', 1)[0].split('=')[1];
                   if (groupName == LDAP.adminGroup) {
                     isInAdminGroup = true;
-                    break;
+
+                    //
+                    // Get an Identity-Token
+                    // if an IDENTITY_TOKEN_SOURCE
+                    // is specified in the config
+                    //
+                    if (IDENTITY_TOKEN_SOURCE) {
+                      const body = JSON.stringify({
+                        username: credential.username,
+                        password: credential.password,
+                        ...CUSTOM_IDENTITY_TOKEN_REQUEST_BODY,
+                      });
+
+                      const options = {
+                        url: IDENTITY_TOKEN_SOURCE,
+                        json: '',
+                        body,
+                        headers: {
+                          'Content-Type': 'application/json',
+                        },
+                        agentOptions: {
+                          rejectUnauthorized: false,
+                        },
+                      };
+
+                      function callback(error, _res, body) {
+                        if (error) {
+                          throw new Error(
+                            `Failed to get ${IDENTITY_TOKEN_SOURCE} Token: ${error}`
+                          );
+                        } else if (body?.error) {
+                          throw new Error(body?.error);
+                        } else {
+                          const parsedBody = JSON.parse(body);
+                          req.session.isAdmin = isInAdminGroup;
+                          req.session.identityToken = parsedBody;
+                          //
+                          // TODO possibly also send identity token
+                          // TODO parsedBody to the client as a cookie
+                          // TODO Github issue #2236
+                          //
+                          res.set('Identity-Token-Payload', body);
+                          res.json(isInAdminGroup);
+
+                          return parsedBody;
+                        }
+                      }
+                      request.post(options, callback);
+                    } else {
+                      req.session.isAdmin = isInAdminGroup;
+                      res.json(isInAdminGroup);
+                    }
+                    //
+                    // END Get an Identity-Token
+                    //
                   }
                 }
+              } else {
+                req.session.isAdmin = isInAdminGroup;
+                res.json(isInAdminGroup);
               }
-
-              request.session.username = credential.username;
-              request.session.isAdmin = isInAdminGroup;
-              response.json(isInAdminGroup);
             });
           });
         }