Verified Commit b910ba53 authored by Aral Balkan's avatar Aral Balkan

Merge branch 'owner'. Add Owner service. Closes #29

  * Registration of owner and persistence of keys (server side).
  * Retrieval of owner’s encrypted private signing key and public signing key.
parents 9f7f3ed6 78c3341b
// Application hooks that run for every service
const logger = require('./hooks/logger');
const logger = require('./hooks/logger')
module.exports = {
before: {
......@@ -31,4 +31,4 @@ module.exports = {
patch: [],
remove: []
}
};
}
......@@ -18,6 +18,8 @@ const path = require('path')
process.env['NODE_CONFIG_DIR'] = path.join(__dirname, 'config/')
const configuration = require('@feathersjs/configuration')
const { globals } = require('./globals')
const express = require('@feathersjs/express')
const primus = require('@feathersjs/primus')
......@@ -34,6 +36,10 @@ const app = express(feathers())
// Load app configuration
app.configure(configuration())
// Set globals
app.configure(globals)
// Enable CORS, security, compression, favicon and body parsing
app.use(cors())
app.use(helmet())
......
module.exports = function(app) {
if(typeof app.channel !== 'function') {
module.exports = function (app) {
if (typeof app.channel !== 'function') {
// If no real-time functionality has been configured just return
return;
return
}
app.on('connection', connection => {
// On a new real-time connection, add it to the anonymous channel
app.channel('anonymous').join(connection);
});
app.channel('anonymous').join(connection)
})
app.on('login', (authResult, { connection }) => {
// connection can be undefined if there is no
// real-time connection, e.g. when logging in via REST
if(connection) {
if (connection) {
// Obtain the logged in user from the connection
// const user = connection.user;
// The connection is no longer anonymous, remove it
app.channel('anonymous').leave(connection);
app.channel('anonymous').leave(connection)
// Add it to the authenticated user channel
app.channel('authenticated').join(connection);
app.channel('authenticated').join(connection)
// Channels can be named anything and joined on any condition
// Channels can be named anything and joined on any condition
// E.g. to send real-time events only to admins use
// if(user.isAdmin) { app.channel('admins').join(connection); }
// If the user has joined e.g. chat rooms
// if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel));
// Easily organize users by email and userid for things like messaging
// app.channel(`emails/${user.email}`).join(channel);
// app.channel(`userIds/$(user.id}`).join(channel);
}
});
})
app.publish((data, hook) => { // eslint-disable-line no-unused-vars
// Here you can add event publishers to channels set up in `channels.js`
......@@ -43,13 +43,13 @@ module.exports = function(app) {
console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line
// e.g. to publish all service events to all authenticated users use
return app.channel('authenticated');
});
return app.channel('authenticated')
})
// Here you can also add service specific event publishers
// e..g the publish the `users` service `created` event to the `admins` channel
// app.service('users').publish('created', () => app.channel('admins'));
// With the userid and email organization from above you can easily select involved users
// app.service('messages').publish(() => {
// return [
......@@ -57,4 +57,4 @@ module.exports = function(app) {
// app.channel(`emails/${data.recipientEmail}`)
// ];
// });
};
}
// Configure globals that can be accessed from a reference to the app
// using app.get('propertyName')
const path = require('path')
const os = require('os')
const fs = require('fs-extra')
const errors = require('@feathersjs/errors')
function setOwnerKeys (app, ownerKeys = null) {
// Try to set the owner’s keys, either from the provided object, if it
// exists, or from the file system, if the data file exists, or simply
// set them to null otherwise.
const ownerKeysFilePath = app.get('ownerKeysFilePath')
let ownerEncryptedPrivateSigningKey = null
let ownerPublicSigningKey = null
function generateConvenienceKeysFrom (mainKeyObject) {
// Update the global state of the app to include the owner’s keys.
ownerEncryptedPrivateSigningKey = {
derivedKeySalt: mainKeyObject.derivedKeySalt,
encryptedPrivateSigningKeyNonce: mainKeyObject.encryptedPrivateSigningKeyNonce,
encryptedPrivateSigningKey: mainKeyObject.encryptedPrivateSigningKey
}
ownerPublicSigningKey = {
publicSigningKey: mainKeyObject.publicSigningKey
}
}
if (ownerKeys === null && fs.existsSync(ownerKeysFilePath)) {
// No keys were passed to us and keys exist on the file system, so
// read them from there.
ownerKeys = fs.readJsonSync(ownerKeysFilePath)
generateConvenienceKeysFrom(ownerKeys)
}
if (ownerKeys !== null) {
// The owner keys are being set for the first time. Persist them.
try {
fs.writeJsonSync(ownerKeysFilePath, ownerKeys, {spaces: 2})
} catch (error) {
throw new errors.GeneralError('Failed to persist owner’s keys.', error)
}
generateConvenienceKeysFrom(ownerKeys)
}
// At this point, if ownerKeys is still null, the owner does not exist.
const ownerExists = ownerKeys !== null
app.set('ownerExists', ownerExists)
app.set('ownerKeysFileExists', ownerExists) // Alias, for completeness.
app.set('ownerKeys', ownerKeys)
app.set('ownerEncryptedPrivateSigningKey', ownerEncryptedPrivateSigningKey)
app.set('ownerPublicSigningKey', ownerPublicSigningKey)
}
function globals (app) {
const dataDirectoryPath = path.join(os.homedir(), '.indie', 'site')
const serverSecretFilePath = path.join(dataDirectoryPath, 'server-secret.json')
const ownerKeysFilePath = path.join(dataDirectoryPath, 'owner-keys.json')
const ownerSettingsFilePath = path.join(dataDirectoryPath, 'owner-settings.json')
// Ensure that the data directory path exists as other parts of the application
// will rely on it being there.
fs.ensureDirSync(dataDirectoryPath)
app.set('dataDirectoryPath', dataDirectoryPath)
app.set('serverSecretFilePath', serverSecretFilePath)
app.set('ownerKeysFilePath', ownerKeysFilePath)
app.set('ownerSettingsFilePath', ownerSettingsFilePath)
// Flag whether the various settings files exist or not so that other parts of
// the application can easily query their state (and, if necessary, update it).
const serverSecretFileExists = fs.existsSync(serverSecretFilePath)
const ownerSettingsFileExists = fs.existsSync(ownerSettingsFilePath)
app.set('serverSecretFileExists', serverSecretFileExists)
app.set('ownerSettingsFileExists', ownerSettingsFileExists)
//
// Owner details
//
setOwnerKeys(app)
}
module.exports = { globals, setOwnerKeys }
/* eslint-disable no-console */
const logger = require('winston')
const app = require('./app')
const portFromConfiguration = app.get('port')
const path = require('path')
const os = require('os')
const fs = require('fs-extra')
const sodium = require('libsodium-wrappers')
const { argv } = require('yargs')
const app = require('./app')
// Set the port:
//
// 1. Via commandline override (--port) if present
// 2. Via environment variable if present
// 3. Via the Feathers configuration file (fallback)
const portFromConfiguration = app.get('port')
const port = argv.port || process.env.PORT || portFromConfiguration
// Start the Feathers (API; REST + sockets) server.
......@@ -28,20 +23,11 @@ function startFeathersServer () {
})
}
// File-based data (e.g., configuration, etc.) is saved at ~/.indie/site
const dataDirectoryPath = path.join(os.homedir(), '.indie', 'site')
const serverSecretFilePath = path.join(dataDirectoryPath, 'server-secret.json')
// On first run of the server only, create a server secret. This
// server secret is used to sign the JSON Web Tokens that we use
// for stateless authentication.
async function createAndPersitServerSecretIfItDoesntAlreadyExist () {
// Check for existence of the file synchronously so as to avoid a possible (if
// highly inprobable) race condition. Since this happens on server start, it
// will not have an impact on performance.
const serverSecretFileExists = fs.existsSync(serverSecretFilePath)
if (!serverSecretFileExists) {
if (!app.get('serverSecretFileExists')) {
//
// Server secret does not exist. This should be the first time the server
// is being run. Create and save the server.
......@@ -58,8 +44,7 @@ async function createAndPersitServerSecretIfItDoesntAlreadyExist () {
// Write the keys file to a data file in a directory for this spike that
// we place in the home directory of the account that this spike is
// running under.
await fs.ensureDir(dataDirectoryPath)
await fs.writeJson(serverSecretFilePath, serverSecretObject)
await fs.writeJson(app.get('serverSecretFilePath'), serverSecretObject)
} catch (error) {
throw new Error('Could not persist the server secret.', error)
}
......
const siteConfiguration = require('./site-configuration/site-configuration.service.js')
const owner = require('./owner/owner.service.js');
module.exports = function (app) {
app.configure(siteConfiguration)
app.configure(owner);
}
const errors = require('@feathersjs/errors')
const { setOwnerKeys } = require('../../globals')
/* eslint-disable no-unused-vars */
class Service {
constructor (options) {
this.options = options || {}
}
// This method is required to be able to pass the feathers app object.
setup (app, appPath) {
this.app = app
}
async find (params) {
return this.app.get('ownerKeys')
}
async get (id, params) {
const getKey = keyName => {
const key = this.app.get(keyName)
if (key === null) throw new errors.NotFound('Key not set.')
return key
}
try {
switch (id) {
case 'encrypted-private-signing-key':
return getKey('ownerEncryptedPrivateSigningKey')
case 'public-signing-key':
return getKey('ownerPublicSigningKey')
default:
throw new errors.NotFound('Key type does not exist.')
}
} catch (error) {
/* re- */ throw error
}
}
// Registers a new owner.
async create (data, params) {
// TODO: Do not use sync method
if (this.app.get('ownerExists')) {
const error = new errors.Forbidden('Owner already exists.')
throw error
}
const ownerKeysObject = {
derivedKeySalt: data.derivedKeySalt,
encryptedPrivateSigningKeyNonce: data.encryptedPrivateSigningKeyNonce,
encryptedPrivateSigningKey: data.encryptedPrivateSigningKey,
publicSigningKey: data.publicSigningKey
}
try {
// Persist the owner’s keys.
//
// Note: this is synchronous but it will happen only once in the lifetime
// of the site so there is no performance issue.
setOwnerKeys(this.app, ownerKeysObject)
return data
} catch (error) {
/* re - */ throw error
}
}
async update (id, data, params) {
return data
}
async patch (id, data, params) {
return data
}
async remove (id, params) {
return { id }
}
}
module.exports = function (options) {
return new Service(options)
}
module.exports.Service = Service
module.exports = {
before: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
after: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};
// Initializes the `owner` service on path `/owner`
const createService = require('./owner.class.js')
const hooks = require('./owner.hooks')
module.exports = function (app) {
const paginate = app.get('paginate')
const options = {
name: 'owner',
paginate
}
// Initialize our service with any options it requires
app.use('/owner', createService(options))
// Get our initialized service so that we can register hooks and filters
const service = app.service('owner')
service.hooks(hooks)
}
const assert = require('assert');
const app = require('../../server/app');
describe('\'owner\' service', () => {
it('registered the service', () => {
const service = app.service('owner');
assert.ok(service, 'Registered the service');
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment