The start of OAuth2 integration, Core, and Kaleidoscope

master
rob 2 years ago
parent ed9d2c64f1
commit c35d0f5a3e

@ -45,6 +45,7 @@ class AdminController extends SiteController {
);
router.use('/content-report',await this.loadChild(path.join(__dirname, 'admin', 'content-report')));
router.use('/core-node',await this.loadChild(path.join(__dirname, 'admin', 'core-node')));
router.use('/host',await this.loadChild(path.join(__dirname, 'admin', 'host')));
router.use('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));

@ -0,0 +1,60 @@
// admin/content-report.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'admin:content-report';
const express = require('express');
// const multer = require('multer');
const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib');
class CoreNodeController extends SiteController {
constructor (dtp) {
super(dtp, DTP_COMPONENT_NAME);
}
async start ( ) {
// const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/upload` });
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'core-node';
return next();
});
router.post('/connect', this.postCoreNodeConnect.bind(this));
router.get('/connect', this.getCoreNodeConnectForm.bind(this));
router.get('/', this.getIndex.bind(this));
return router;
}
async postCoreNodeConnect (req, res, next) {
// const { coreNode: coreNodeService } = this.dtp.services;
try {
} catch (error) {
this.log.error('failed to create Core Node connection request', { error });
return next(error);
}
}
async getCoreNodeConnectForm (req, res) {
res.render('admin/core-node/connect');
}
async getIndex (req, res) {
res.render('admin/core-node/index');
}
}
module.exports = async (dtp) => {
let controller = new CoreNodeController(dtp);
return controller;
};

@ -0,0 +1,64 @@
// hive.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'hive';
const path = require('path');
const express = require('express');
const { SiteController } = require('../../lib/site-lib');
class HiveController extends SiteController {
constructor (dtp) {
super(dtp, DTP_COMPONENT_NAME);
this.services = [ ];
}
async start ( ) {
const router = express.Router();
this.dtp.app.use('/hive', router);
router.use(
async (req, res, next) => {
res.locals.currentView = 'hive';
res.locals.hiveView = 'home';
/*
* TODO: H1V3 authentication before processing request (HTTP Bearer token)
*/
return next();
},
);
router.use('/kaleidoscope',await this.loadChild(path.join(__dirname, 'hive', 'kaleidoscope')));
this.services.push({ name: 'kaleidoscope', url: '/hive/kaleidoscope' });
router.get('/', this.getHiveRoot.bind(this));
return router;
}
async getHiveRoot (req, res) {
res.status(200).json({
component: DTP_COMPONENT_NAME,
host: this.dtp.pkg.name,
description: this.dtp.pkg.description,
version: this.dtp.pkg.version,
services: this.services,
});
}
}
module.exports = {
slug: 'hive',
name: 'hive',
create: async (dtp) => {
let controller = new HiveController(dtp);
return controller;
},
};

@ -0,0 +1,72 @@
// hive/kaleidoscope.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'hive:kaleidoscope';
const express = require('express');
const { SiteController } = require('../../../lib/site-lib');
class HostController extends SiteController {
constructor (dtp) {
super(dtp, DTP_COMPONENT_NAME);
this.methods = [
{
name: 'postEvent',
url: '/kaleidoscope/event',
method: 'POST',
},
];
}
async start ( ) {
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'hive';
res.locals.hiveView = 'kaleidoscope';
return next();
});
router.post('/core-node/connect', this.postCoreNodeConnect.bind(this));
router.post('/event', this.postEvent.bind(this));
router.get('/', this.getKaleidoscopeRoot.bind(this));
return router;
}
async postCoreNodeConnect (req, res, next) {
const { coreNode: coreNodeService } = this.dtp.services;
try {
await coreNodeService.connect(req.body);
} catch (error) {
this.log.error('failed to create Core Node connection', { error });
return next(error);
}
}
async postEvent (req, res) {
this.log.debug('kaleidoscope event received', { event: req.body.event });
this.emit('kaleidoscope:event', req, res);
res.status(200).json({ success: true });
}
async getKaleidoscopeRoot (req, res) {
res.status(200).json({
component: DTP_COMPONENT_NAME,
version: this.dtp.pkg.version,
services: this.services,
methods: this.methods,
});
}
}
module.exports = async (dtp) => {
let controller = new HostController(dtp);
return controller;
};

@ -0,0 +1,37 @@
// core-node-request.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
/*
* Used for authenticating responses received and gathering performance and use
* metrics for communications with Cores.
*
* When a request is created, an authentication token is generated and
* information about the request is stored. This also provides the request ID.
*
* When a resonse is received for a request, this record is fetched. The token
* claimed status and value are checked. Information about the response is
* recorded, and request execution time information is recorded.
*/
const CoreNodeRequestSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: 1 },
core: { type: Schema.ObjectId, required: true, ref: 'CoreNode' },
token: {
value: { type: String, required: true },
claimed: { type: Boolean, default: false, required: true },
},
url: { type: String },
response: {
received: { type: Date },
elapsed: { type: Number },
isError: { type: Boolean, default: false },
},
});
module.exports = mongoose.model('CoreNodeRequest', CoreNodeRequestSchema);

@ -0,0 +1,18 @@
// core-node.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CoreNodeSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: 1 },
address: {
host: { type: String, required: true },
port: { type: Number, min: 1, max: 65535, required: true },
},
});
module.exports = mongoose.model('CoreNode', CoreNodeSchema);

@ -0,0 +1,97 @@
// core-node.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const uuidv4 = require('uuid').v4;
const mongoose = require('mongoose');
const fetch = require('node-fetch'); // jshint ignore:line
const CoreNode = mongoose.model('CoreNode');
const CoreNodeRequest = mongoose.model('CoreNodeRequest');
const { SiteService, SiteError } = require('../../lib/site-lib');
class CoreNodeService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
}
async create (coreDefinition) {
const core = new CoreNode();
core.created = new Date();
core.address = { };
if (!coreDefinition.host) {
throw new SiteError(406, 'Must provide Core Node host address');
}
core.address.host = coreDefinition.host.trim();
if (!coreDefinition.port) {
throw new SiteError(406, 'Must provide Core Node TCP port number');
}
coreDefinition.port = parseInt(coreDefinition.port, 10);
if (coreDefinition.port < 1 || coreDefinition.port > 65535) {
throw new SiteError(406, 'Core Node port number out of range');
}
await core.save();
return core.toObject();
}
async broadcast (request) {
const results = [ ];
await CoreNode
.find()
.cursor()
.eachAsync(async (core) => {
try {
const response = await this.sendRequest(core, request);
results.push({ coreId: core._id, request, response });
} catch (error) {
this.log.error('failed to send Core Node request', { core: core._id, request: request.url, error });
}
});
return results;
}
async sendRequest (core, request) {
const requestUrl = `https://${core.address.host}:${core.address.port}${request.url}`;
const req = new CoreNodeRequest();
req.created = new Date();
req.core = core._id;
req.token = {
value: uuidv4(),
claimed: false,
};
req.url = request.url;
await req.save();
try {
const response = await fetch(requestUrl, {
method: request.method,
body: request.body,
});
const json = await response.json();
return { request: req.toObject(), response: json };
} catch (error) {
this.log.error('failed to send Core Node request', { core: core._id, request: request.url, error });
throw error;
}
return req.toObject();
}
}
module.exports = {
slug: 'core-node',
name: 'coreNode',
create: (dtp) => { return new CoreNodeService(dtp); },
};

@ -0,0 +1,191 @@
// oauth2.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const passport = require('passport');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const uuidv4 = require('uuid').v4;
const oauth2orize = require('oauth2orize');
const { SiteService/*, SiteError*/ } = require('../../lib/site-lib');
class OAuth2Service extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
this.models = { };
/*
* OAuth2Client Model
*/
const ClientSchema = new Schema({
created: { type: Date, default: Date.now, required: true },
updated: { type: Date, default: Date.now, required: true },
secret: { type: String, required: true },
redirectURI: { type: String, required: true },
});
this.log.info('registering OAuth2Client model');
this.models.Client = mongoose.model('OAuth2Client', ClientSchema);
/*
* OAuth2AuthorizationCode model
*/
const AuthorizationCodeSchema = new Schema({
code: { type: String, required: true, index: 1 },
clientId: { type: Schema.ObjectId, required: true, index: 1 },
redirectURI: { type: String, required: true },
user: { type: Schema.ObjectId, required: true, index: 1 },
scope: { type: [String], required: true },
});
this.log.info('registering OAuth2AuthorizationCode model');
this.models.AuthorizationCode = mongoose.model('OAuth2AuthorizationCode', AuthorizationCodeSchema);
/*
* OAuth2AccessToken model
*/
const AccessTokenSchema = new Schema({
token: { type: String, required: true, unique: true, index: 1 },
user: { type: Schema.ObjectId, required: true, index: 1 },
clientId: { type: Schema.ObjectId, required: true, index: 1 },
scope: { type: [String], required: true },
});
this.log.info('registering OAuth2AccessToken model');
this.models.AccessToken = mongoose.model('OAuth2AccessToken', AccessTokenSchema);
/*
* Create OAuth2 server instance
*/
const options = { };
this.log.info('creating OAuth2 server instance', { options });
this.server = oauth2orize.createServer(options);
this.server.grant(oauth2orize.grant.code(this.processGrant.bind(this)));
this.server.exchange(oauth2orize.exchange.code(this.processExchange.bind(this)));
/*
* Register client serialization callbacks
*/
this.log.info('registering OAuth2 client serialization routines');
this.server.serializeClient(this.serializeClient.bind(this));
this.server.deserializeClient(this.deserializeClient.bind(this));
}
async serializeClient (client, done) {
return done(null, client.id);
}
async deserializeClient (clientId, done) {
try {
const client = await this.models.Client.findOne({ _id: clientId }).lean();
return done(null, client);
} catch (error) {
this.log.error('failed to deserialize OAuth2 client', { clientId, error });
return done(error);
}
}
attachRoutes (app) {
const { session: sessionService } = this.dtp.services;
const requireLogin = sessionService.authCheckMiddleware({ requireLogin: true });
app.get(
'/dialog/authorize',
requireLogin,
this.server.authorize(this.processAuthorize.bind(this)),
this.renderAuthorizeDialog.bind(this),
);
app.post(
'/dialog/authorize/decision',
requireLogin,
this.server.decision(),
);
app.post(
'/token',
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
this.server.token(),
this.server.errorHandler(),
);
}
async renderAuthorizeDialog (req, res) {
res.locals.transactionID = req.oauth2.transactionID;
res.locals.client = req.oauth2.client;
res.render('oauth2/authorize-dialog');
}
async processAuthorize (clientID, redirectURI, done) {
try {
const client = await this.models.Clients.findOne({ clientID });
if (!client) {
return done(null, false);
}
if (client.redirectUri !== redirectURI) {
return done(null, false);
}
return done(null, client, client.redirectURI);
} catch (error) {
this.log.error('failed to process OAuth2 authorize', { error });
return done(error);
}
}
async processGrant (client, redirectURI, user, ares, done) {
try {
var code = uuidv4();
var ac = new this.models.AuthorizationCode({
code,
clientId: client.id,
redirectURI,
user: user.id,
scope: ares.scope,
});
await ac.save();
return done(null, code);
} catch (error) {
this.log.error('failed to process OAuth2 grant', { error });
return done(error);
}
}
async processExchange (client, code, redirectURI, done) {
try {
const ac = await this.models.AuthorizationCode.findOne({ code });
if (client.id !== ac.clientId) {
return done(null, false);
}
if (redirectURI !== ac.redirectUri) {
return done(null, false);
}
var token = uuidv4();
var at = new this.models.AccessToken({
token,
user: ac.userId,
clientId: ac.clientId,
scope: ac.scope,
});
await at.save();
return done(null, token);
} catch (error) {
this.log.error('failed to process OAuth2 exchange', { error });
return done(error);
}
}
}
module.exports = {
slug: 'oauth2',
name: 'oauth2',
create: (dtp) => { return new OAuth2Service(dtp); },
};

@ -27,6 +27,11 @@ ul.uk-nav.uk-nav-default
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'core-node') })
a(href="/admin/core-node")
span.nav-item-icon
i.fas.fa-project-diagram
span.uk-margin-small-left Core Nodes
li(class={ 'uk-active': (adminView === 'host') })
a(href="/admin/host")
span.nav-item-icon

@ -0,0 +1,18 @@
extends ../layouts/main
block content
form(method="POST", action="/admin/core-node/connect").uk-form
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h1.uk-card-title Connect to New Core
.uk-card-body
.uk-margin
label(for="host").uk-form-label Address
input(id="host", name="host", placeholder="Enter host name or address", required).uk-input
.uk-margin
label(for="port").uk-form-label Port Number
input(id="port", name="port", min="1", max="65535", step="1", value="4200", required).uk-input
.uk-card-footer
button(type="submit").uk-button.uk-button-primary Send Request

@ -0,0 +1,14 @@
extends ../layouts/main
block content
h1 Core Nodes
a(href="/admin/core-node/connect").uk-button.uk-button-primary Connect Core
p You can register with one or more Core nodes to exchange information with those nodes.
if Array.isArray(coreNodes) && (coreNodes.length > 0)
ul.uk-list
each node in coreNodes
pre= JSON.stringify(node, null, 2)
else
p There are no registered core nodes.

@ -54,6 +54,7 @@
"node-fetch": "2",
"nodemailer": "^6.7.2",
"numeral": "^2.0.6",
"oauth2orize": "^1.11.1",
"otplib": "^12.0.1",
"passport": "^0.5.2",
"passport-local": "^1.0.0",

@ -1,8 +1,11 @@
#!/bin/bash
MINIO_CI_CD=1
MINIO_ROOT_USER="webapp"
MINIO_ROOT_PASSWORD="302888b9-c3d8-40f5-92de-6a3c57186af5"
export MINIO_ROOT_USER MINIO_ROOT_PASSWORD
export MINIO_ROOT_USER MINIO_ROOT_PASSWORD MINIO_CI_CD
forever start --killSignal=SIGINT app/workers/host-services.js
forever start --killSignal=SIGINT app/workers/reeeper.js

@ -2795,7 +2795,7 @@ date-now@^0.1.4:
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@~2.6.4:
debug@2.6.9, debug@2.x.x, debug@^2.2.0, debug@^2.3.3, debug@~2.6.4:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@ -6061,6 +6061,15 @@ o-stream@^0.3.0:
resolved "https://registry.yarnpkg.com/o-stream/-/o-stream-0.3.0.tgz#204d27bc3fb395164507d79b381e91752e8daedc"
integrity sha512-gbzl6qCJZ609x/M2t25HqCYQagFzWYCtQ84jcuObGr+V8D1Am4EVubkF4J+XFs6ukfiv96vNeiBb8FrbbMZYiQ==
oauth2orize@^1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/oauth2orize/-/oauth2orize-1.11.1.tgz#00b6cafe2036a0a3aab0380627dc7cfd5b5e9a9c"
integrity sha512-9dSx/Gwm0J2Rvj4RH9+h7iXVnRXZ6biwWRgb2dCeQhCosODS0nYdM9I/G7BUGsjbgn0pHjGcn1zcCRtzj2SlRA==
dependencies:
debug "2.x.x"
uid2 "0.0.x"
utils-merge "1.x.x"
object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -8335,6 +8344,11 @@ uid2@0.0.3:
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=
uid2@0.0.x:
version "0.0.4"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44"
integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==
uikit@^3.11.1:
version "3.11.1"
resolved "https://registry.yarnpkg.com/uikit/-/uikit-3.11.1.tgz#3f0b47f4b2e7610375c5f7cdbf550d086c81e22f"
@ -8528,7 +8542,7 @@ util@^0.12.3:
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
utils-merge@1.0.1:
utils-merge@1.0.1, utils-merge@1.x.x:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=

Loading…
Cancel
Save