diff --git a/CHANGELOG.md b/CHANGELOG.md index dbb9a631cb..da28a47487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.0.0-alpha.8 +- [Auth] User-provider ([#261](https://github.com/Unleash/unleash/issues/261)) +- [Auth] Document how to secure Unleash ([#234](https://github.com/Unleash/unleash/issues/234)) +- [Auth] Admin UI should handle 401 ([#232](https://github.com/Unleash/unleash/issues/232)) +- [Auth] Client API authentication ([#231](https://github.com/Unleash/unleash/issues/231)) +- [Auth] Handle 403 (Forbidden) with custom auth. +- [Auth] Support sign out ([#288](https://github.com/Unleash/unleash/issues/288)) + ## 3.0.0-alpha.7 - Bugfix: Should not allow creation of archived toggle #284 diff --git a/docs/getting-started.md b/docs/getting-started.md index f31ae85ef2..7e98bfbada 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -41,6 +41,11 @@ Available unleash options includes: - **serverMetrics** (boolean) - Use this option to turn of prometheus metrics. - **preHook** (function) - This is a hook if you need to provide any middlewares to express before `unleash` adds any. Express app instance is injected as first arguement. - **preRouterHook** (function) - Use this to register custom express middlewares before the `unleash` specific routers are added. This is typically how you would register custom middlewares to handle authentication. +- **secret** (string) - Set this when you want to secure unleash. Used to encrypt the user session. +- **adminAuthentication** (string) - Use this when implementing cusotm admin authentication [securing-unleash](./securing-unleash.md). Legal values are: + - `none` - Will disable autentication all together + - `unsecure` - (default) Will use simple cookie based authentication. UI will require the user to specify an email in order to use unleash. + - `custom` - Use this when you implement your own custom authentication logic. ## How do I configure the log output? diff --git a/docs/securing-unleash.md b/docs/securing-unleash.md new file mode 100644 index 0000000000..c8e69aed76 --- /dev/null +++ b/docs/securing-unleash.md @@ -0,0 +1,70 @@ +# Secure Unleash +The Unleash API is split in two different paths: `/api/client` and `/api/admin`. +This makes it easy to have different authentication strategy for the admin interface and the client-api used by the applications integrating with Unleash. + +## General settings +Unleash uses an encrypted cookie to maintain a user session. This allows users to be logged in across instances of Unleash. To protect this cookie you should specify the `secret` option when starting unleash.- + +## Securing the Admin API +In order to secure the Admin API you have to tell Unleash that you are using a custom admin authentication and implement your authentication logic as a preHook. You should also set the secret option to a protected secret in your system. + +```javascript +const unleash = require('unleash-server'); +const myCustomAdminAuth = require('./auth-hook'); + +unleash.start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + secret: 'super-duper-secret', + adminAuthentication: 'custom', + preRouterHook: myCustomAdminAuth +}).then(unleash => { + console.log(`Unleash started on http://localhost:${unleash.app.get('port')}`); +}); +``` + +Examples on custom authentication hooks: +- [google-auth-hook.js](https://github.com/Unleash/unleash/blob/master/examples/google-auth-hook.js) +- [basic-auth-hook.js](https://github.com/Unleash/unleash/blob/master/examples/basic-auth-hook.js) + + +## Securing the Client API +A common way to support client access is to use pre shared secrets. This can be solved by having clients send a shared key in a http header with every client requests to the Unleash API. All official Unleash clients should support this. + +In the [Java client](https://github.com/Unleash/unleash-client-java#custom-http-headers) this looks like: + +```java +UnleashConfig unleashConfig = UnleashConfig.builder() + .appName("my-app") + .instanceId("my-instance-1") + .unleashAPI(unleashAPI) + .customHttpHeader("Authorization", "12312Random") + .build(); +``` + +On the unleash server side you need to implement a preRouter hook which verifies that all calls to `/api/client` includes this pre shared key in the defined header. This could look something like this: + +```javascript +const unleash = require('unleash-server'); +const sharedSecret = '12312Random'; + +unleash.start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + enableLegacyRoutes: false, + preRouterHook: (app) => { + app.use('/api/client', (req, res, next) => { + if(req.headers.authorization !== sharedSecret) { + res.sendStatus(401); + } else { + next() + } + }); + } +}).then(unleash => { + console.log(`Unleash started on http://localhost:${unleash.app.get('port')}`); +}); +``` + +[client-auth-unleash.js](https://github.com/Unleash/unleash/blob/master/examples/client-auth-unleash.js) + + +PS! Remember to disable legacy route with by setting the `enableLegacyRoutes` option to false. This will require all your clients to be on v3.x. diff --git a/examples/basic-auth-hook.js b/examples/basic-auth-hook.js new file mode 100644 index 0000000000..7d09b27278 --- /dev/null +++ b/examples/basic-auth-hook.js @@ -0,0 +1,30 @@ +'use strict'; + +const auth = require('basic-auth'); +const { User } = require('../lib/server-impl.js'); + +function basicAuthentication(app) { + app.use('/api/admin/', (req, res, next) => { + const credentials = auth(req); + + if (credentials) { + // you will need to do some verification of credentials here. + const user = new User({ email: `${credentials.name}@domain.com` }); + req.user = user; + next(); + } else { + return res + .status('401') + .set({ 'WWW-Authenticate': 'Basic realm="example"' }) + .end('access denied'); + } + }); + + app.use((req, res, next) => { + // Updates active sessions every hour + req.session.nowInHours = Math.floor(Date.now() / 3600e3); + next(); + }); +} + +module.exports = basicAuthentication; diff --git a/examples/basic-auth-unleash.js b/examples/basic-auth-unleash.js new file mode 100644 index 0000000000..0b525d87c6 --- /dev/null +++ b/examples/basic-auth-unleash.js @@ -0,0 +1,19 @@ +'use strict'; + +// const unleash = require('unleash-server'); +const unleash = require('../lib/server-impl.js'); + +const basicAuth = require('./basic-auth-hook'); + +unleash + .start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + secret: 'super-duper-secret', + adminAuthentication: 'custom', + preRouterHook: basicAuth, + }) + .then(server => { + console.log( + `Unleash started on http://localhost:${server.app.get('port')}` + ); + }); diff --git a/examples/client-auth-unleash.js b/examples/client-auth-unleash.js new file mode 100644 index 0000000000..13ad5b741e --- /dev/null +++ b/examples/client-auth-unleash.js @@ -0,0 +1,27 @@ +'use strict'; + +// const unleash = require('unleash-server'); +const unleash = require('../lib/server-impl.js'); + +// You typically will not hard-code this value in your code! +const sharedSecret = '12312Random'; + +unleash + .start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + enableLegacyRoutes: false, + preRouterHook: app => { + app.use('/api/client', (req, res, next) => { + if (req.headers.authorization === sharedSecret) { + next(); + } else { + res.sendStatus(401); + } + }); + }, + }) + .then(server => { + console.log( + `Unleash started on http://localhost:${server.app.get('port')}` + ); + }); diff --git a/examples/google-auth-hook.js b/examples/google-auth-hook.js new file mode 100644 index 0000000000..635b7e1cc4 --- /dev/null +++ b/examples/google-auth-hook.js @@ -0,0 +1,85 @@ +'use strict'; + +/** + * Google OAath 2.0 + * + * You should read Using OAuth 2.0 to Access Google APIs: + * https://developers.google.com/identity/protocols/OAuth2 + * + * This example assumes that all users authenticating via + * google should have access. You would proably limit access + * to users you trust. + * + * The implementation assumes the following environement variables: + * + * - GOOGLE_CLIENT_ID + * - GOOGLE_CLIENT_SECRET + * - GOOGLE_CALLBACK_URL + */ + +// const { User, AuthenticationRequired } = require('unleash-server'); +const { User, AuthenticationRequired } = require('../lib/server-impl.js'); + +const passport = require('passport'); +const GoogleOAuth2Strategy = require('passport-google-auth').Strategy; + +passport.use( + new GoogleOAuth2Strategy( + { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL, + }, + + (accessToken, refreshToken, profile, done) => { + done( + null, + new User({ + name: profile.displayName, + email: profile.emails[0].value, + }) + ); + } + ) +); + +function enableGoogleOauth(app) { + app.use(passport.initialize()); + app.use(passport.session()); + + passport.serializeUser((user, done) => done(null, user)); + passport.deserializeUser((user, done) => done(null, user)); + app.get('/api/admin/login', passport.authenticate('google')); + + app.get( + '/api/auth/callback', + passport.authenticate('google', { + failureRedirect: '/api/admin/error-login', + }), + (req, res) => { + // Successful authentication, redirect to your app. + res.redirect('/'); + } + ); + + app.use('/api/admin/', (req, res, next) => { + if (req.user) { + next(); + } else { + // Instruct unleash-frontend to pop-up auth dialog + return res + .status('401') + .json( + new AuthenticationRequired({ + path: '/api/admin/login', + type: 'custom', + message: `You have to identify yourself in order to use Unleash. + Click the button and follow the instructions.`, + }) + ) + .end(); + } + }); +} + +module.exports = enableGoogleOauth; diff --git a/examples/google-auth-unleash.js b/examples/google-auth-unleash.js new file mode 100644 index 0000000000..7b0e2a26da --- /dev/null +++ b/examples/google-auth-unleash.js @@ -0,0 +1,19 @@ +'use strict'; + +// const unleash = require('unleash-server'); +const unleash = require('../lib/server-impl.js'); + +const enableGoogleOauth = require('./google-auth-hook'); + +unleash + .start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + secret: 'super-duper-secret', + adminAuthentication: 'custom', + preRouterHook: enableGoogleOauth, + }) + .then(server => { + console.log( + `Unleash started on http://localhost:${server.app.get('port')}` + ); + }); diff --git a/lib/app.js b/lib/app.js index 0be1fe787a..ca819c9ed8 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 0000000000..4c1a60f2d8 --- /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 a6f3fdcbc4..e0743a1020 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 0000000000..0276f28576 --- /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 indentify 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 3e5f7deee7..59411c41b9 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 7fe231f4e3..75a495c53e 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/metrics.js b/lib/routes/admin-api/metrics.js index f2b057f4a6..ed68eeb195 100644 --- a/lib/routes/admin-api/metrics.js +++ b/lib/routes/admin-api/metrics.js @@ -4,7 +4,11 @@ const { Router } = require('express'); const logger = require('../../logger')('/admin-api/metrics.js'); const ClientMetrics = require('../../client-metrics'); -const { catchLogAndSendErrorResponse } = require('./route-utils'); + +const catchLogAndSendErrorResponse = (err, res) => { + logger.error(err); + res.status(500).end(); +}; exports.router = function(config) { const { @@ -67,10 +71,7 @@ exports.router = function(config) { clientApplicationsStore .upsert(input) .then(() => res.status(202).end()) - .catch(e => { - logger.error(e); - res.status(500).end(); - }); + .catch(err => catchLogAndSendErrorResponse(err, res)); }); function toLookup(metaData) { diff --git a/lib/routes/admin-api/metrics.test.js b/lib/routes/admin-api/metrics.test.js index 4fa5baafac..6d60600284 100644 --- a/lib/routes/admin-api/metrics.test.js +++ b/lib/routes/admin-api/metrics.test.js @@ -93,3 +93,31 @@ test('should return metrics for all toggles', t => { t.true(metrics.lastMinute !== undefined); }); }); + +test('should return applications', t => { + t.plan(2); + const { request, stores } = getSetup(); + const appName = '123!23'; + + stores.clientApplicationsStore.upsert({ appName }); + + return request + .get(`/api/admin/metrics/applications/`) + .expect(200) + .expect(res => { + const metrics = res.body; + t.true(metrics.applications.length === 1); + t.true(metrics.applications[0].appName === appName); + }); +}); + +test('should store application', t => { + t.plan(0); + const { request } = getSetup(); + const appName = '123!23'; + + return request + .post(`/api/admin/metrics/applications/${appName}`) + .send({ appName }) + .expect(202); +}); diff --git a/lib/routes/admin-api/route-utils.js b/lib/routes/admin-api/route-utils.js deleted file mode 100644 index 23f93dd627..0000000000 --- a/lib/routes/admin-api/route-utils.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const logger = require('../../logger')('route-utils.js'); - -const catchLogAndSendErrorResponse = (err, res) => { - logger.error(err); - res.status(500).end(); -}; - -module.exports = { catchLogAndSendErrorResponse }; diff --git a/lib/routes/admin-api/user.js b/lib/routes/admin-api/user.js new file mode 100644 index 0000000000..ec32f57e6e --- /dev/null +++ b/lib/routes/admin-api/user.js @@ -0,0 +1,27 @@ +'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(); + } + }); + + router.get('/logout', (req, res) => { + if (req.session) { + req.session = null; + } + res.redirect('/'); + }); + + return router; +}; diff --git a/lib/routes/admin-api/user.test.js b/lib/routes/admin-api/user.test.js new file mode 100644 index 0000000000..a436ec5495 --- /dev/null +++ b/lib/routes/admin-api/user.test.js @@ -0,0 +1,57 @@ +'use strict'; + +const { test } = require('ava'); +const store = require('./../../../test/fixtures/store'); +const supertest = require('supertest'); +const getApp = require('../../app'); +const User = require('../../user'); + +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + +const currentUser = new User({ email: 'test@mail.com' }); + +function getSetup() { + const base = `/random${Math.round(Math.random() * 1000)}`; + const stores = store.createStores(); + const app = getApp({ + baseUriPath: base, + stores, + eventBus, + preHook: a => { + a.use((req, res, next) => { + req.user = currentUser; + next(); + }); + }, + }); + + return { + base, + strategyStore: stores.strategyStore, + request: supertest(app), + }; +} + +test('should return current user', t => { + t.plan(1); + const { request, base } = getSetup(); + + return request + .get(`${base}/api/admin/user`) + .expect(200) + .expect('Content-Type', /json/) + .expect(res => { + t.true(res.body.email === currentUser.email); + }); +}); + +test('should logout and redirect', t => { + t.plan(0); + const { request, base } = getSetup(); + + return request + .get(`${base}/api/admin/user/logout`) + .expect(302) + .expect('Location', '/'); +}); diff --git a/lib/server-impl.js b/lib/server-impl.js index da4ff2687b..983d99661a 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 0000000000..d070bfac22 --- /dev/null +++ b/lib/user.js @@ -0,0 +1,20 @@ +'use strict'; + +const gravatar = require('gravatar'); +const Joi = require('joi'); + +module.exports = class User { + constructor({ name, email, imageUrl } = {}) { + Joi.assert( + email, + Joi.string() + .email() + .required(), + 'Email' + ); + 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 0000000000..5f1c45bd6f --- /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 "value" 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 01e841e951..8f4126aa96 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ }, "scripts": { "start": "node server.js", + "start:google": "node examples/google-auth-unleash.js", "start:dev": "NODE_ENV=development supervisor --ignore ./node_modules/ server.js", "start:dev:pg": "pg_virtualenv npm run start:dev:pg-chain", "start:dev:pg-chain": "export DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@localhost:$PGPORT/postgres ; db-migrate up && npm run start:dev", @@ -67,19 +68,22 @@ "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", "prometheus-gc-stats": "^0.5.0", "response-time": "^2.3.2", "serve-favicon": "^2.3.0", - "unleash-frontend": "^3.0.0-alpha.4", + "unleash-frontend": "^3.0.0-alpha.5", "yallist": "^3.0.2", "yargs": "^10.0.3" }, 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 0000000000..20c3c63cc8 --- /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 0000000000..e18f1dee65 --- /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 f9c9cc0815..b3b725e668 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 ffb6582852..1ee858d4c1 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 0000000000..55df2fcb07 --- /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 0000000000..f9f59dd98d --- /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 9e4921b4d1..fbf5c7f9c8 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(), - ]); -} - -function setupDatabase(stores) { - return Promise.all( - createStrategies(stores).concat( - createFeatures(stores) - .concat(createClientInstance(stores)) - .concat(createApplications(stores)) - ) - ); } module.exports = { - setupApp(name) { - return createApp(name).then(app => - resetDatabase(app.stores) - .then(() => setupDatabase(app.stores)) - .then(() => app) - ); + 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(), + }; + }, + + async setupAppWithCustomAuth(name, preHook) { + const stores = await dbInit(name); + const app = createApp(stores, 'custom', preHook); + + return { + request: supertest.agent(app), + destroy: () => stores.db.destroy(), + }; }, }; diff --git a/test/fixtures/fake-client-applications-store.js b/test/fixtures/fake-client-applications-store.js index 7d5d49ebfd..2f4f7b0921 100644 --- a/test/fixtures/fake-client-applications-store.js +++ b/test/fixtures/fake-client-applications-store.js @@ -1,6 +1,12 @@ 'use strict'; +const _appliations = []; + module.exports = () => ({ - upsert: () => Promise.resolve(), - getApplications: () => Promise.resolve([]), + upsert: app => { + _appliations.push(app); + return Promise.resolve(); + }, + getApplications: () => Promise.resolve(_appliations), + getApplication: appName => _appliations.filter(a => a.name === appName)[0], }); diff --git a/yarn.lock b/yarn.lock index e2ac57a7d2..414c504368 100644 --- a/yarn.lock +++ b/yarn.lock @@ -324,7 +324,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 +756,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 +790,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 +858,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 +932,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 +1568,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 +1587,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 +2334,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 +2377,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" @@ -3097,6 +3165,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 +3443,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 +3759,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 +4033,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 +4183,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 +4261,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 +4565,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 +4930,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 +5400,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" @@ -5646,9 +5775,9 @@ unique-temp-dir@^1.0.0: os-tmpdir "^1.0.1" uid2 "0.0.3" -unleash-frontend@^3.0.0-alpha.4: - version "3.0.0-alpha.4" - resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.0.0-alpha.4.tgz#d252c84fd6d9bd9972402013a6109f695207894d" +unleash-frontend@^3.0.0-alpha.5: + version "3.0.0-alpha.5" + resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.0.0-alpha.5.tgz#ef4c2bb9e24ba07465b1737098b92b6a036df282" dependencies: debug "^3.1.0" immutable "^3.8.1" @@ -5769,6 +5898,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 +6027,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 +6056,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"