Skip to content

Commit

Permalink
Merge pull request #100 from flowforge/roll-in-plugins
Browse files Browse the repository at this point in the history
Move storage/auth/auditLogger/theme into launcher
  • Loading branch information
knolleary authored Jan 25, 2023
2 parents cd05457 + 8d0448b commit b0d6297
Show file tree
Hide file tree
Showing 32 changed files with 2,006 additions and 19 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# FlowForge Wrapper for Node-RED
# FlowForge Launcher for Node-RED

Custom launcher to start Node-RED is a loadable set of settings and to capture logs so they can be streamed to the admin app
This is the launcher FlowForge uses to run instances of Node-RED. It dynamically
generates the Node-RED settings based on the associated Project's settings.

The launcher starts its own HTTP service to allow the FlowForge platform to remotely
control it.

### Configure

Expand Down
41 changes: 41 additions & 0 deletions lib/auditLogger/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
*
*/

const got = require('got')

module.exports = (settings) => {
const baseURL = settings.loggingURL
const projectID = settings.projectID
const token = settings.token

const logger = function (msg) {
if (/^(comms\.|.*\.get$)/.test(msg.event)) {
// Ignore comms events and any .get event that is just reading data
return
}
if (/^auth/.test(msg.event) && !/^auth.log/.test(msg.event)) {
return
}
if (msg.user) {
msg.user = msg.user.userId
}
delete msg.username
delete msg.level

got.post(baseURL + '/' + projectID + '/audit', {
json: msg,
responseType: 'json',
headers: {
'user-agent': 'FlowForge Audit Logging v0.1',
authorization: 'Bearer ' + token
}
})
.catch(err => {
// ignore errors for now
console.log(err)
})
}

return logger
}
77 changes: 77 additions & 0 deletions lib/auth/adminAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { OAuth2 } = require('oauth')
const { Strategy } = require('./strategy')

module.exports = (options) => {
['clientID', 'clientSecret', 'forgeURL', 'baseURL'].forEach(prop => {
if (!options[prop]) {
throw new Error(`Missing configuration option ${prop}`)
}
})

const clientID = options.clientID
const clientSecret = options.clientSecret
const forgeURL = options.forgeURL
const baseURL = options.baseURL

const callbackURL = `${baseURL}/auth/strategy/callback`
const authorizationURL = `${forgeURL}/account/authorize`
const tokenURL = `${forgeURL}/account/token`
const userInfoURL = `${forgeURL}/api/v1/user`

const oa = new OAuth2(clientID, clientSecret, '', authorizationURL, tokenURL)

const version = require('../../package.json').version

const activeUsers = {}

function addUser (username, profile, refreshToken, expiresIn) {
if (activeUsers[username]) {
clearTimeout(activeUsers[username].refreshTimeout)
}
activeUsers[username] = {
profile,
refreshToken,
expiresIn
}
activeUsers[username].refreshTimeout = setTimeout(function () {
oa.getOAuthAccessToken(refreshToken, {
grant_type: 'refresh_token'
}, function (err, accessToken, refreshToken, results) {
if (err) {
delete activeUsers[username]
} else {
addUser(username, profile, refreshToken, results.expires_in)
}
})
}, expiresIn * 1000)
}

return {
type: 'strategy',
strategy: {
name: 'FlowForge',
autoLogin: true,
label: 'Sign in',
strategy: Strategy,
options: {
authorizationURL,
tokenURL,
callbackURL,
userInfoURL,
scope: `editor-${version}`,
clientID,
clientSecret,
pkce: true,
state: true,
verify: function (accessToken, refreshToken, params, profile, done) {
profile.permissions = [params.scope || 'read']
addUser(profile.username, profile, refreshToken, params.expires_in)
done(null, profile)
}
}
},
users: async function (username) {
return activeUsers[username] && activeUsers[username].profile
}
}
}
71 changes: 71 additions & 0 deletions lib/auth/httpAuthMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const { Passport } = require('passport')
const { Strategy } = require('./strategy')

let options
let passport

module.exports = {
init (_options) {
options = _options
return (req, res, next) => {
try {
if (req.session.ffSession) {
next()
} else {
req.session.redirectTo = req.originalUrl
passport.authenticate('FlowForge', { session: false })(req, res, next)
}
} catch (err) {
console.log(err.stack)
throw err
}
}
},

setupAuthRoutes (app) {
if (!options) {
// If `init` has not been called, then the flowforge-user auth type
// has not been selected. No need to setup any further routes.
return
}
// 'app' is RED.httpNode - the express app that handles all http routes
// exposed by the flows.

passport = new Passport()
app.use(passport.initialize())

const callbackURL = `${options.baseURL}/_ffAuth/callback`
const authorizationURL = `${options.forgeURL}/account/authorize`
const tokenURL = `${options.forgeURL}/account/token`
const userInfoURL = `${options.forgeURL}/api/v1/user`
const version = require('../../package.json').version

passport.use('FlowForge', new Strategy({
authorizationURL,
tokenURL,
callbackURL,
userInfoURL,
scope: `editor-${version}`,
clientID: options.clientID,
clientSecret: options.clientSecret,
pkce: true,
state: true
}, function (accessToken, refreshToken, params, profile, done) {
done(null, profile)
}))

app.get('/_ffAuth/callback', passport.authenticate('FlowForge', {
session: false
}), (req, res) => {
req.session.user = req.user
req.session.ffSession = true
if (req.session?.redirectTo) {
const redirectTo = req.session.redirectTo
delete req.session.redirectTo
res.redirect(redirectTo)
} else {
res.redirect('/')
}
})
}
}
10 changes: 10 additions & 0 deletions lib/auth/httpAuthPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { setupAuthRoutes } = require('./httpAuthMiddleware')

module.exports = (RED) => {
RED.plugins.registerPlugin('ff-auth-plugin', {
onadd: () => {
RED.log.info('FlowForge HTTP Authentication Plugin loaded')
setupAuthRoutes(RED.httpNode)
}
})
}
33 changes: 33 additions & 0 deletions lib/auth/strategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const util = require('util')
const OAuth2Strategy = require('passport-oauth2')

function Strategy (options, verify) {
this.options = options
this._base = Object.getPrototypeOf(Strategy.prototype)
this._base.constructor.call(this, this.options, verify)
this.name = 'FlowForge'
}

util.inherits(Strategy, OAuth2Strategy)

Strategy.prototype.userProfile = function (accessToken, done) {
this._oauth2.useAuthorizationHeaderforGET(true)
this._oauth2.get(this.options.userInfoURL, accessToken, (err, body) => {
if (err) {
return done(err)
}
try {
const json = JSON.parse(body)
done(null, {
username: json.username,
image: json.avatar,
name: json.name,
userId: json.id
})
} catch (e) {
done(e)
}
})
}

module.exports = { Strategy }
7 changes: 1 addition & 6 deletions lib/launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,11 @@ class Launcher {
} else if (this.settings.nodesDir && typeof this.settings.nodesDir === 'string') {
nodesDir.push(this.settings.nodesDir)
}
nodesDir.push(path.join(require.main.path, 'node_modules', '@flowforge', 'nr-theme').replace(/\\/g, '/')) // MVP: fixed to loading FF theme
nodesDir.push(path.join(require.main.path, '..', 'nr-theme').replace(/\\/g, '/')) // MVP: fixed to loading FF theme
nodesDir.push(path.join(require.main.path, 'node_modules', '@flowforge', 'nr-project-nodes').replace(/\\/g, '/'))
nodesDir.push(path.join(require.main.path, '..', 'nr-project-nodes').replace(/\\/g, '/'))
nodesDir.push(path.join(require.main.path, 'node_modules', '@flowforge', 'nr-file-nodes').replace(/\\/g, '/'))
nodesDir.push(path.join(require.main.path, '..', 'nr-file-nodes').replace(/\\/g, '/'))
nodesDir.push(path.join(require.main.path, 'node_modules', '@flowforge', 'nr-auth').replace(/\\/g, '/'))
nodesDir.push(path.join(require.main.path, '..', 'nr-auth').replace(/\\/g, '/'))
nodesDir.push(path.join(require.main.path, 'node_modules', '@flowforge', 'nr-storage').replace(/\\/g, '/'))
nodesDir.push(path.join(require.main.path, '..', 'nr-storage').replace(/\\/g, '/'))
nodesDir.push(require.main.path)

this.settings.nodesDir = nodesDir

Expand Down
8 changes: 4 additions & 4 deletions lib/runtimeSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function getSettingsFile (settings) {
projectSettings.httpNodeAuth = `httpNodeAuth: ${JSON.stringify(settings.settings.httpNodeAuth)},`
} else if (settings.settings.httpNodeAuth?.type === 'flowforge-user') {
authMiddlewareRequired = true
projectSettings.setupAuthMiddleware = `const flowforgeAuthMiddleware = require('@flowforge/nr-auth/middleware').init({
projectSettings.setupAuthMiddleware = `const flowforgeAuthMiddleware = require('@flowforge/nr-launcher/authMiddleware').init({
type: 'flowforge-user',
baseURL: '${settings.baseURL}',
forgeURL: '${settings.forgeURL}',
Expand Down Expand Up @@ -179,7 +179,7 @@ ${projectSettings.setupAuthMiddleware}
module.exports = {
flowFile: 'flows.json',
flowFilePretty: true,
adminAuth: require('@flowforge/nr-auth')({
adminAuth: require('@flowforge/nr-launcher/adminAuth')({
baseURL: '${settings.baseURL}',
forgeURL: '${settings.forgeURL}',
clientID: '${settings.clientID}',
Expand All @@ -196,7 +196,7 @@ module.exports = {
httpServerOptions: {
"trust proxy": true
},
storageModule: require('@flowforge/nr-storage'),
storageModule: require('@flowforge/nr-launcher/storage'),
httpStorage: {
projectID: '${settings.projectID}',
baseURL: '${settings.storageURL}',
Expand Down Expand Up @@ -237,7 +237,7 @@ module.exports = {
},
auditLogger: {
level: 'off', audit: true, handler: require('@flowforge/nr-audit-logger'),
level: 'off', audit: true, handler: require('@flowforge/nr-launcher/auditLogger'),
loggingURL: '${settings.auditURL}',
projectID: '${settings.projectID}',
token: '${settings.projectToken}'
Expand Down
92 changes: 92 additions & 0 deletions lib/storage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const got = require('got')

let settings

module.exports = {
init: (nrSettings) => {
settings = nrSettings.httpStorage || {}

if (Object.keys(settings) === 0) {
const err = Promise.reject(new Error('No settings for flow storage module found'))
// err.catch(err => {})
return err
}

const projectID = settings.projectID

// console.log(settings)
// console.log(settings.baseURL + "/" + projectID + "/")

this._client = got.extend({
prefixUrl: settings.baseURL + '/' + projectID + '/',
headers: {
'user-agent': 'FlowForge HTTP Storage v0.1',
authorization: 'Bearer ' + settings.token
},
timeout: {
request: 10000
}
})

return Promise.resolve()
},
getFlows: async () => {
return this._client.get('flows').json()
},
saveFlows: async (flow) => {
return this._client.post('flows', {
json: flow,
responseType: 'json'
})
},
getCredentials: async () => {
return this._client.get('credentials').json()
},
saveCredentials: async (credentials) => {
return this._client.post('credentials', {
json: credentials,
responseType: 'json'
})
},
getSettings: () => {
return this._client.get('settings').json()
},
saveSettings: (settings) => {
return this._client.post('settings', {
json: settings,
responseType: 'json'
})
},
getSessions: () => {
this._client.get('sessions').json()
},
saveSessions: (sessions) => {
return this._client.post('sessions', {
json: sessions,
responseType: 'json'
})
},
getLibraryEntry: (type, name) => {
return this._client.get('library/' + type, {
searchParams: {
name
}
}).then(entry => {
if (entry.headers['content-type'].startsWith('application/json')) {
return JSON.parse(entry.body)
} else {
return entry.body
}
})
},
saveLibraryEntry: (type, name, meta, body) => {
return this._client.post('library/' + type, {
json: {
name,
meta,
body
},
responseType: 'json'
})
}
}
Loading

0 comments on commit b0d6297

Please sign in to comment.