diff --git a/docs/securing-unleash.md b/docs/securing-unleash.md new file mode 100644 index 000000000000..bc657af3644e --- /dev/null +++ b/docs/securing-unleash.md @@ -0,0 +1,3 @@ +# Securing Unleash + +TODO: write about how to secure `/api/client` and `/api/admin` \ No newline at end of file diff --git a/lib/app.js b/lib/app.js index 0be1fe787a3b..ca819c9ed81c 100644 --- a/lib/app.js +++ b/lib/app.js @@ -11,6 +11,7 @@ const unleashSession = require('./middleware/session'); const responseTime = require('./middleware/response-time'); const requestLogger = require('./middleware/request-logger'); const validator = require('./middleware/validator'); +const simpleAuthentication = require('./middleware/simple-authentication'); module.exports = function(config) { const app = express(); @@ -38,6 +39,10 @@ module.exports = function(config) { app.use(baseUriPath, express.static(config.publicFolder)); } + if (config.adminAuthentication === 'unsecure') { + simpleAuthentication(app); + } + if (typeof config.preRouterHook === 'function') { config.preRouterHook(app); } diff --git a/lib/authentication-required.js b/lib/authentication-required.js new file mode 100644 index 000000000000..4c1a60f2d8d5 --- /dev/null +++ b/lib/authentication-required.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = class AuthenticationRequired { + constructor({ type, path, message }) { + this.type = type; + this.path = path; + this.message = message; + } +}; diff --git a/lib/extract-user.js b/lib/extract-user.js index a6f3fdcbc494..e0743a1020d8 100644 --- a/lib/extract-user.js +++ b/lib/extract-user.js @@ -1,6 +1,7 @@ 'use strict'; function extractUsername(req) { - return req.cookies.username || 'unknown'; + return req.user ? req.user.email : 'unknown'; } + module.exports = extractUsername; diff --git a/lib/middleware/simple-authentication.js b/lib/middleware/simple-authentication.js new file mode 100644 index 000000000000..c5d4f9d64e74 --- /dev/null +++ b/lib/middleware/simple-authentication.js @@ -0,0 +1,48 @@ +'use strict'; + +const User = require('../user'); +const AuthenticationRequired = require('../authentication-required'); + +function unsecureAuthentication(app) { + app.post('/api/admin/login', (req, res) => { + const user = req.body; + req.session.user = new User({ email: user.email }); + res + .status(200) + .json(req.session.user) + .end(); + }); + + app.use('/api/admin/', (req, res, next) => { + if (req.session.user && req.session.user.email) { + req.user = req.session.user; + } + next(); + }); + + app.use('/api/admin/', (req, res, next) => { + if (req.user) { + next(); + } else { + return res + .status('401') + .json( + new AuthenticationRequired({ + path: '/api/admin/login', + type: 'unsecure', + message: + 'You have to indetify yourself in order to use Unleash.', + }) + ) + .end(); + } + }); + + app.use((req, res, next) => { + // Updates active sessions every hour + req.session.nowInHours = Math.floor(Date.now() / 3600e3); + next(); + }); +} + +module.exports = unsecureAuthentication; diff --git a/lib/options.js b/lib/options.js index 3e5f7deee75a..59411c41b95f 100644 --- a/lib/options.js +++ b/lib/options.js @@ -15,6 +15,7 @@ const DEFAULT_OPTIONS = { enableRequestLogger: isDev(), secret: 'UNLEASH-SECRET', sessionAge: THIRTY_DAYS, + adminAuthentication: 'unsecure', }; module.exports = { diff --git a/lib/routes/admin-api/index.js b/lib/routes/admin-api/index.js index 7fe231f4e3d0..75a495c53e62 100644 --- a/lib/routes/admin-api/index.js +++ b/lib/routes/admin-api/index.js @@ -7,6 +7,7 @@ const featureArchive = require('./archive.js'); const events = require('./event.js'); const strategies = require('./strategy'); const metrics = require('./metrics'); +const user = require('./user'); const apiDef = { version: 2, @@ -31,6 +32,7 @@ exports.router = config => { router.use('/strategies', strategies.router(config)); router.use('/events', events.router(config)); router.use('/metrics', metrics.router(config)); + router.use('/user', user.router(config)); return router; }; diff --git a/lib/routes/admin-api/user.js b/lib/routes/admin-api/user.js new file mode 100644 index 000000000000..a958795d18a1 --- /dev/null +++ b/lib/routes/admin-api/user.js @@ -0,0 +1,20 @@ +'use strict'; + +const { Router } = require('express'); + +exports.router = function() { + const router = Router(); + + router.get('/', (req, res) => { + if (req.user) { + return res + .status(200) + .json(req.user) + .end(); + } else { + return res.status(404).end(); + } + }); + + return router; +}; diff --git a/lib/server-impl.js b/lib/server-impl.js index da4ff2687bf0..983d99661add 100644 --- a/lib/server-impl.js +++ b/lib/server-impl.js @@ -9,6 +9,8 @@ const getApp = require('./app'); const { startMonitoring } = require('./metrics'); const { createStores } = require('./db'); const { createOptions } = require('./options'); +const User = require('./user'); +const AuthenticationRequired = require('./authentication-required'); function createApp(options) { // Database dependecies (statefull) @@ -44,4 +46,6 @@ function start(opts) { module.exports = { start, + User, + AuthenticationRequired, }; diff --git a/lib/user.js b/lib/user.js new file mode 100644 index 000000000000..af102f459d17 --- /dev/null +++ b/lib/user.js @@ -0,0 +1,14 @@ +'use strict'; + +const gravatar = require('gravatar'); +const assert = require('assert'); + +module.exports = class User { + constructor({ name, email, imageUrl } = {}) { + assert(email, 'Email is required'); + this.email = email; + this.name = name; + this.imageUrl = + imageUrl || gravatar.url(email, { s: '42', d: 'retro' }); + } +}; diff --git a/lib/user.test.js b/lib/user.test.js new file mode 100644 index 000000000000..7f356bc932ee --- /dev/null +++ b/lib/user.test.js @@ -0,0 +1,28 @@ +'use strict'; + +const { test } = require('ava'); +const User = require('./user'); + +test('should create user', t => { + const user = new User({ name: 'ole', email: 'some@email.com' }); + t.is(user.name, 'ole'); + t.is(user.email, 'some@email.com'); + t.is( + user.imageUrl, + '//www.gravatar.com/avatar/d8ffeba65ee5baf57e4901690edc8e1b?s=42&d=retro' + ); +}); + +test('should require email', t => { + const error = t.throws(() => { + const user = new User(); // eslint-disable-line + }, Error); + + t.is(error.message, 'Email is required'); +}); + +test('Should create user with only email defined', t => { + const user = new User({ email: 'some@email.com' }); + + t.is(user.email, 'some@email.com'); +}); diff --git a/package.json b/package.json index 01e841e95157..752f2295e50f 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ ] }, "dependencies": { + "assert": "^1.4.1", "async": "^2.1.5", "body-parser": "^1.18.2", "commander": "^2.9.0", @@ -67,12 +68,15 @@ "errorhandler": "^1.5.0", "express": "^4.16.2", "express-validator": "^4.3.0", + "gravatar": "^1.6.0", "install": "^0.10.1", "joi": "^13.0.1", "knex": "^0.14.0", "log4js": "^2.0.0", "moment": "^2.19.3", "parse-database-url": "^0.3.0", + "passport": "^0.4.0", + "passport-google-auth": "^1.0.2", "pg": "^7.4.0", "pkginfo": "^0.4.1", "prom-client": "^10.0.4", diff --git a/test/e2e/api/admin/feature.auth.e2e.test.js b/test/e2e/api/admin/feature.auth.e2e.test.js new file mode 100644 index 000000000000..20c3c63cc8ba --- /dev/null +++ b/test/e2e/api/admin/feature.auth.e2e.test.js @@ -0,0 +1,36 @@ +'use strict'; + +const { test } = require('ava'); +const { setupAppWithAuth } = require('./../../helpers/test-helper'); + +test.serial('creates new feature toggle with createdBy', async t => { + t.plan(1); + const { request, destroy } = await setupAppWithAuth('feature_api_auth'); + // Login + await request.post('/api/admin/login').send({ + email: 'user@mail.com', + }); + + // create toggle + await request.post('/api/admin/features').send({ + name: 'com.test.Username', + enabled: false, + strategies: [{ name: 'default' }], + }); + + await request + .get('/api/admin/events') + .expect(res => { + t.true(res.body.events[0].createdBy === 'user@mail.com'); + }) + .then(destroy); +}); + +test.serial('should require authenticated user', async t => { + t.plan(0); + const { request, destroy } = await setupAppWithAuth('feature_api_auth'); + return request + .get('/api/admin/features') + .expect(401) + .then(destroy); +}); diff --git a/test/e2e/api/admin/feature.custom-auth.e2e.test.js b/test/e2e/api/admin/feature.custom-auth.e2e.test.js new file mode 100644 index 000000000000..e18f1dee6505 --- /dev/null +++ b/test/e2e/api/admin/feature.custom-auth.e2e.test.js @@ -0,0 +1,62 @@ +'use strict'; + +const { test } = require('ava'); +const { setupAppWithCustomAuth } = require('./../../helpers/test-helper'); +const AuthenticationRequired = require('./../../../../lib/authentication-required'); +const User = require('./../../../../lib/user'); + +test.serial('should require authenticated user', async t => { + t.plan(0); + const preHook = app => { + app.use('/api/admin/', (req, res) => + res + .status('401') + .json( + new AuthenticationRequired({ + path: '/api/admin/login', + type: 'custom', + message: `You have to identify yourself.`, + }) + ) + .end() + ); + }; + const { request, destroy } = await setupAppWithCustomAuth( + 'feature_api_custom_auth', + preHook + ); + return request + .get('/api/admin/features') + .expect(401) + .then(destroy); +}); + +test.serial('creates new feature toggle with createdBy', async t => { + t.plan(1); + const user = new User({ email: 'custom-user@mail.com' }); + + const preHook = app => { + app.use('/api/admin/', (req, res, next) => { + req.user = user; + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + 'feature_api_custom_auth', + preHook + ); + + // create toggle + await request.post('/api/admin/features').send({ + name: 'com.test.Username', + enabled: false, + strategies: [{ name: 'default' }], + }); + + await request + .get('/api/admin/events') + .expect(res => { + t.true(res.body.events[0].createdBy === user.email); + }) + .then(destroy); +}); diff --git a/test/e2e/api/admin/feature.e2e.test.js b/test/e2e/api/admin/feature.e2e.test.js index f9c9cc081528..b3b725e668cb 100644 --- a/test/e2e/api/admin/feature.e2e.test.js +++ b/test/e2e/api/admin/feature.e2e.test.js @@ -50,22 +50,18 @@ test.serial('creates new feature toggle', async t => { .then(destroy); }); -test.serial('creates new feature toggle with createdBy', async t => { +test.serial('creates new feature toggle with createdBy unknown', async t => { t.plan(1); const { request, destroy } = await setupApp('feature_api_serial'); - await request - .post('/api/admin/features') - .send({ - name: 'com.test.Username', - enabled: false, - strategies: [{ name: 'default' }], - }) - .set('Cookie', ['username=ivaosthu']) - .set('Content-Type', 'application/json'); + await request.post('/api/admin/features').send({ + name: 'com.test.Username', + enabled: false, + strategies: [{ name: 'default' }], + }); await request .get('/api/admin/events') .expect(res => { - t.true(res.body.events[0].createdBy === 'ivaosthu'); + t.true(res.body.events[0].createdBy === 'unknown'); }) .then(destroy); }); diff --git a/test/e2e/api/client/feature.e2e.test.js b/test/e2e/api/client/feature.e2e.test.js index ffb65828522d..1ee858d4c125 100644 --- a/test/e2e/api/client/feature.e2e.test.js +++ b/test/e2e/api/client/feature.e2e.test.js @@ -1,5 +1,36 @@ 'use strict'; const { test } = require('ava'); +const { setupApp } = require('./../../helpers/test-helper'); -test.todo('e2e client feature'); +test.serial('returns three feature toggles', async t => { + const { request, destroy } = await setupApp('feature_api_client'); + return request + .get('/api/client/features') + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + t.true(res.body.features.length === 3); + }) + .then(destroy); +}); + +test.serial('gets a feature by name', async t => { + t.plan(0); + const { request, destroy } = await setupApp('feature_api_client'); + return request + .get('/api/client/features/featureX') + .expect('Content-Type', /json/) + .expect(200) + .then(destroy); +}); + +test.serial('cant get feature that dose not exist', async t => { + t.plan(0); + const { request, destroy } = await setupApp('feature_api_client'); + return request + .get('/api/client/features/myfeature') + .expect('Content-Type', /json/) + .expect(404) + .then(destroy); +}); diff --git a/test/e2e/helpers/database-init.js b/test/e2e/helpers/database-init.js new file mode 100644 index 000000000000..55df2fcb077c --- /dev/null +++ b/test/e2e/helpers/database-init.js @@ -0,0 +1,70 @@ +'use strict'; + +const migrator = require('../../../migrator'); +const { createStores } = require('../../../lib/db'); +const { createDb } = require('../../../lib/db/db-pool'); + +const dbState = require('./database.json'); + +require('db-migrate-shared').log.silence(true); + +// because of migrator bug +delete process.env.DATABASE_URL; + +// because of db-migrate bug (https://github.com/Unleash/unleash/issues/171) +process.setMaxListeners(0); + +async function resetDatabase(stores) { + return Promise.all([ + stores.db('strategies').del(), + stores.db('features').del(), + stores.db('client_applications').del(), + stores.db('client_instances').del(), + ]); +} + +async function setupDatabase(stores) { + const updates = []; + updates.push(...createStrategies(stores.strategyStore)); + updates.push(...createFeatures(stores.featureToggleStore)); + updates.push(...createClientInstance(stores.clientInstanceStore)); + updates.push(...createApplications(stores.clientApplicationsStore)); + + await Promise.all(updates); +} + +function createStrategies(store) { + return dbState.strategies.map(s => store._createStrategy(s)); +} + +function createApplications(store) { + return dbState.applications.map(a => store.upsert(a)); +} + +function createClientInstance(store) { + return dbState.clientInstances.map(i => store.insert(i)); +} + +function createFeatures(store) { + return dbState.features.map(f => store._createFeature(f)); +} + +module.exports = async function init(databaseSchema = 'test') { + const options = { + databaseUrl: require('./database-config').getDatabaseUrl(), + databaseSchema, + minPool: 0, + maxPool: 0, + }; + + const db = createDb(options); + + await db.raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`); + await migrator(options); + await db.destroy(); + const stores = await createStores(options); + await resetDatabase(stores); + await setupDatabase(stores); + + return stores; +}; diff --git a/test/e2e/helpers/database.json b/test/e2e/helpers/database.json new file mode 100644 index 000000000000..f9f59dd98df6 --- /dev/null +++ b/test/e2e/helpers/database.json @@ -0,0 +1,113 @@ +{ + "strategies": [ + { + "name": "default", + "description": "Default on or off Strategy.", + "parameters": [] + }, + { + "name": "usersWithEmail", + "description": "Active for users defined in the comma-separated emails-parameter.", + "parameters": [{ + "name": "emails", + "type": "string" + }] + } + ], + "applications": [ + { + "appName": "demo-app-1", + "strategies": ["default"] + }, + { + "appName": "demo-app-2", + "strategies": ["default", + "extra" + ], + "description": "hello" + } + ], + "clientInstances": [ + { + "appName": "demo-app-1", + "instanceId": "test-1", + "strategies": ["default"], + "started": 1516026938494, + "interval": 10 + }, + { + "appName": "demo-seed-2", + "instanceId": "test-2", + "strategies": ["default"], + "started": 1516026938494, + "interval": 10 + } + ], + "features": [ + { + "name": "featureX", + "description": "the #1 feature", + "enabled": true, + "strategies": [{ + "name": "default", + "parameters": {} + }] + }, + { + "name": "featureY", + "description": "soon to be the #1 feature", + "enabled": false, + "strategies": [{ + "name": "baz", + "parameters": { + "foo": "bar" + } + }] + }, + { + "name": "featureZ", + "description": "terrible feature", + "enabled": true, + "strategies": [{ + "name": "baz", + "parameters": { + "foo": "rab" + } + }] + }, + { + "name": "featureArchivedX", + "description": "the #1 feature", + "enabled": true, + "archived": true, + "strategies": [{ + "name": "default", + "parameters": {} + }] + }, + { + "name": "featureArchivedY", + "description": "soon to be the #1 feature", + "enabled": false, + "archived": true, + "strategies": [{ + "name": "baz", + "parameters": { + "foo": "bar" + } + }] + }, + { + "name": "featureArchivedZ", + "description": "terrible feature", + "enabled": true, + "archived": true, + "strategies": [{ + "name": "baz", + "parameters": { + "foo": "rab" + } + }] + } + ] +} \ No newline at end of file diff --git a/test/e2e/helpers/test-helper.js b/test/e2e/helpers/test-helper.js index 9e4921b4d1b9..fbf5c7f9c84e 100644 --- a/test/e2e/helpers/test-helper.js +++ b/test/e2e/helpers/test-helper.js @@ -2,198 +2,52 @@ process.env.NODE_ENV = 'test'; -// because of db-migrate bug (https://github.com/Unleash/unleash/issues/171) -process.setMaxListeners(0); - const supertest = require('supertest'); -const migrator = require('../../../migrator'); -const { createStores } = require('../../../lib/db'); -const { createDb } = require('../../../lib/db/db-pool'); -const getApp = require('../../../lib/app'); -require('db-migrate-shared').log.silence(true); -// because of migrator bug -delete process.env.DATABASE_URL; +const getApp = require('../../../lib/app'); +const dbInit = require('./database-init'); const { EventEmitter } = require('events'); const eventBus = new EventEmitter(); -function createApp(databaseSchema = 'test') { - const options = { - databaseUrl: require('./database-config').getDatabaseUrl(), - databaseSchema, - minPool: 0, - maxPool: 0, - }; - const db = createDb({ - databaseUrl: options.databaseUrl, - minPool: 0, - maxPool: 0, +function createApp(stores, adminAuthentication = 'none', preHook) { + return getApp({ + stores, + eventBus, + preHook, + adminAuthentication, + secret: 'super-secret', + sessionAge: 4000, }); - - return db - .raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`) - .then(() => migrator(options)) - .then(() => { - db.destroy(); - const stores = createStores(options); - const app = getApp({ stores, eventBus }); - return { - stores, - request: supertest(app), - destroy() { - return stores.db.destroy(); - }, - }; - }); -} - -function createStrategies(stores) { - return [ - { - name: 'default', - description: 'Default on or off Strategy.', - parameters: [], - }, - { - name: 'usersWithEmail', - description: - 'Active for users defined in the comma-separated emails-parameter.', - parameters: [{ name: 'emails', type: 'string' }], - }, - ].map(strategy => stores.strategyStore._createStrategy(strategy)); -} - -function createApplications(stores) { - return [ - { - appName: 'demo-app-1', - strategies: ['default'], - }, - { - appName: 'demo-app-2', - strategies: ['default', 'extra'], - description: 'hello', - }, - ].map(client => stores.clientApplicationsStore.upsert(client)); -} - -function createClientInstance(stores) { - return [ - { - appName: 'demo-app-1', - instanceId: 'test-1', - strategies: ['default'], - started: Date.now(), - interval: 10, - }, - { - appName: 'demo-seed-2', - instanceId: 'test-2', - strategies: ['default'], - started: Date.now(), - interval: 10, - }, - ].map(client => stores.clientInstanceStore.insert(client)); -} - -function createFeatures(stores) { - return [ - { - name: 'featureX', - description: 'the #1 feature', - enabled: true, - strategies: [{ name: 'default', parameters: {} }], - }, - { - name: 'featureY', - description: 'soon to be the #1 feature', - enabled: false, - strategies: [ - { - name: 'baz', - parameters: { - foo: 'bar', - }, - }, - ], - }, - { - name: 'featureZ', - description: 'terrible feature', - enabled: true, - strategies: [ - { - name: 'baz', - parameters: { - foo: 'rab', - }, - }, - ], - }, - { - name: 'featureArchivedX', - description: 'the #1 feature', - enabled: true, - archived: true, - strategies: [{ name: 'default', parameters: {} }], - }, - { - name: 'featureArchivedY', - description: 'soon to be the #1 feature', - enabled: false, - archived: true, - strategies: [ - { - name: 'baz', - parameters: { - foo: 'bar', - }, - }, - ], - }, - { - name: 'featureArchivedZ', - description: 'terrible feature', - enabled: true, - archived: true, - strategies: [ - { - name: 'baz', - parameters: { - foo: 'rab', - }, - }, - ], - }, - ].map(feature => stores.featureToggleStore._createFeature(feature)); } -function resetDatabase(stores) { - return Promise.all([ - stores.db('strategies').del(), - stores.db('features').del(), - stores.db('client_applications').del(), - stores.db('client_instances').del(), - ]); -} +module.exports = { + async setupApp(name) { + const stores = await dbInit(name); + const app = createApp(stores); + + return { + request: supertest.agent(app), + destroy: () => stores.db.destroy(), + }; + }, + async setupAppWithAuth(name) { + const stores = await dbInit(name); + const app = createApp(stores, 'unsecure'); + + return { + request: supertest.agent(app), + destroy: () => stores.db.destroy(), + }; + }, -function setupDatabase(stores) { - return Promise.all( - createStrategies(stores).concat( - createFeatures(stores) - .concat(createClientInstance(stores)) - .concat(createApplications(stores)) - ) - ); -} + async setupAppWithCustomAuth(name, preHook) { + const stores = await dbInit(name); + const app = createApp(stores, 'custom', preHook); -module.exports = { - setupApp(name) { - return createApp(name).then(app => - resetDatabase(app.stores) - .then(() => setupDatabase(app.stores)) - .then(() => app) - ); + return { + request: supertest.agent(app), + destroy: () => stores.db.destroy(), + }; }, }; diff --git a/yarn.lock b/yarn.lock index e2ac57a7d288..b44c7d7d82b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -298,6 +298,12 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" +assert@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + ast-types@0.x.x: version "0.9.14" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.14.tgz#d34ba5dffb9d15a44351fd2a9d82e4ab2838b5ba" @@ -324,7 +330,7 @@ async@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" -async@~2.1.2: +async@~2.1.2, async@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" dependencies: @@ -756,6 +762,10 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base64url@2.0.0, base64url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -786,6 +796,10 @@ bluebird@^3.0.0, bluebird@^3.1.1, bluebird@^3.4.6: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" +blueimp-md5@^2.3.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.10.0.tgz#02f0843921f90dca14f5b8920a38593201d6964d" + body-parser@1.18.2, body-parser@^1.18.2: version "1.18.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" @@ -850,6 +864,10 @@ buf-compare@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/buf-compare/-/buf-compare-1.0.1.tgz#fef28da8b8113a0a0db4430b0b6467b69730b34a" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-writer@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-1.0.1.tgz#22a936901e3029afcd7547eb4487ceb697a3bf08" @@ -920,6 +938,10 @@ camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -1552,6 +1574,13 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" + dependencies: + base64url "^2.0.0" + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -1564,6 +1593,10 @@ element-class@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e" +email-validator@^1.0.7: + version "1.1.1" + resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-1.1.1.tgz#b07f3be7bac1dc099bc43e75f6ae399f552d5a80" + empower-core@^0.6.1: version "0.6.2" resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-0.6.2.tgz#5adef566088e31fba80ba0a36df47d7094169144" @@ -2307,6 +2340,29 @@ globby@^6.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +google-auth-library@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" + dependencies: + gtoken "^1.2.1" + jws "^3.1.4" + lodash.noop "^3.0.1" + request "^2.74.0" + +google-p12-pem@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" + dependencies: + node-forge "^0.7.1" + +googleapis@^16.0.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576" + dependencies: + async "~2.1.4" + google-auth-library "~0.10.0" + string-template "~1.0.0" + got@^6.7.1: version "6.7.1" resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -2327,6 +2383,24 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +gravatar@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/gravatar/-/gravatar-1.6.0.tgz#8bdc9b786ca725a8e7076416d1731f8d3331c976" + dependencies: + blueimp-md5 "^2.3.0" + email-validator "^1.0.7" + querystring "0.2.0" + yargs "^6.0.0" + +gtoken@^1.2.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8" + dependencies: + google-p12-pem "^0.1.0" + jws "^3.0.0" + mime "^1.4.1" + request "^2.72.0" + handlebars@^4.0.3: version "4.0.11" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" @@ -2616,6 +2690,10 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, i version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" @@ -3097,6 +3175,23 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jwa@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" + dependencies: + base64url "2.0.0" + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.9" + safe-buffer "^5.0.1" + +jws@^3.0.0, jws@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" + dependencies: + base64url "^2.0.0" + jwa "^1.1.4" + safe-buffer "^5.0.1" + keygrip@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" @@ -3358,6 +3453,10 @@ lodash.merge@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" +lodash.noop@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" + lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3670,6 +3769,10 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" +node-forge@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" + node-fs@~0.1.5: version "0.1.7" resolved "https://registry.yarnpkg.com/node-fs/-/node-fs-0.1.7.tgz#32323cccb46c9fbf0fc11812d45021cc31d325bb" @@ -3940,6 +4043,12 @@ os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + os-locale@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" @@ -4084,6 +4193,24 @@ parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" +passport-google-auth@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938" + dependencies: + googleapis "^16.0.0" + passport-strategy "1.x" + +passport-strategy@1.x, passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + +passport@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811" + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -4144,6 +4271,10 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" @@ -4444,6 +4575,10 @@ query-string@^4.2.2: object-assign "^4.1.0" strict-uri-encode "^1.0.0" +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + randomatic@^1.1.3: version "1.1.7" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" @@ -4805,7 +4940,7 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -request@^2.0.0, request@^2.74.0, request@^2.79.0: +request@^2.0.0, request@^2.72.0, request@^2.74.0, request@^2.79.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -5275,6 +5410,10 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" +string-template@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -5699,6 +5838,12 @@ util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + utile@0.3.x: version "0.3.0" resolved "https://registry.yarnpkg.com/utile/-/utile-0.3.0.tgz#1352c340eb820e4d8ddba039a4fbfaa32ed4ef3a" @@ -5769,6 +5914,10 @@ when@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/when/-/when-2.0.1.tgz#8d872fe15e68424c91b4b724e848e0807dab6642" +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -5894,6 +6043,12 @@ yallist@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + yargs-parser@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.0.0.tgz#21d476330e5a82279a4b881345bf066102e219c6" @@ -5917,6 +6072,24 @@ yargs@^10.0.3: y18n "^3.2.1" yargs-parser "^8.0.0" +yargs@^6.0.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"