mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-28 00:06:53 +01:00
Merge pull request #283 from Unleash/admin_auth
[Auth] Support for providing custom Oauth 2.0 provider
This commit is contained in:
commit
93410de68d
@ -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
|
||||
|
||||
|
@ -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?
|
||||
|
||||
|
70
docs/securing-unleash.md
Normal file
70
docs/securing-unleash.md
Normal file
@ -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.
|
30
examples/basic-auth-hook.js
Normal file
30
examples/basic-auth-hook.js
Normal file
@ -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;
|
19
examples/basic-auth-unleash.js
Normal file
19
examples/basic-auth-unleash.js
Normal file
@ -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')}`
|
||||
);
|
||||
});
|
27
examples/client-auth-unleash.js
Normal file
27
examples/client-auth-unleash.js
Normal file
@ -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')}`
|
||||
);
|
||||
});
|
85
examples/google-auth-hook.js
Normal file
85
examples/google-auth-hook.js
Normal file
@ -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;
|
19
examples/google-auth-unleash.js
Normal file
19
examples/google-auth-unleash.js
Normal file
@ -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')}`
|
||||
);
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
9
lib/authentication-required.js
Normal file
9
lib/authentication-required.js
Normal file
@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = class AuthenticationRequired {
|
||||
constructor({ type, path, message }) {
|
||||
this.type = type;
|
||||
this.path = path;
|
||||
this.message = message;
|
||||
}
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
function extractUsername(req) {
|
||||
return req.cookies.username || 'unknown';
|
||||
return req.user ? req.user.email : 'unknown';
|
||||
}
|
||||
|
||||
module.exports = extractUsername;
|
||||
|
48
lib/middleware/simple-authentication.js
Normal file
48
lib/middleware/simple-authentication.js
Normal file
@ -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;
|
@ -15,6 +15,7 @@ const DEFAULT_OPTIONS = {
|
||||
enableRequestLogger: isDev(),
|
||||
secret: 'UNLEASH-SECRET',
|
||||
sessionAge: THIRTY_DAYS,
|
||||
adminAuthentication: 'unsecure',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 };
|
27
lib/routes/admin-api/user.js
Normal file
27
lib/routes/admin-api/user.js
Normal file
@ -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;
|
||||
};
|
57
lib/routes/admin-api/user.test.js
Normal file
57
lib/routes/admin-api/user.test.js
Normal file
@ -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', '/');
|
||||
});
|
@ -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,
|
||||
};
|
||||
|
20
lib/user.js
Normal file
20
lib/user.js
Normal file
@ -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' });
|
||||
}
|
||||
};
|
28
lib/user.test.js
Normal file
28
lib/user.test.js
Normal file
@ -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');
|
||||
});
|
@ -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"
|
||||
},
|
||||
|
36
test/e2e/api/admin/feature.auth.e2e.test.js
Normal file
36
test/e2e/api/admin/feature.auth.e2e.test.js
Normal file
@ -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);
|
||||
});
|
62
test/e2e/api/admin/feature.custom-auth.e2e.test.js
Normal file
62
test/e2e/api/admin/feature.custom-auth.e2e.test.js
Normal file
@ -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);
|
||||
});
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
70
test/e2e/helpers/database-init.js
Normal file
70
test/e2e/helpers/database-init.js
Normal file
@ -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;
|
||||
};
|
113
test/e2e/helpers/database.json
Normal file
113
test/e2e/helpers/database.json
Normal file
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
@ -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(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
10
test/fixtures/fake-client-applications-store.js
vendored
10
test/fixtures/fake-client-applications-store.js
vendored
@ -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],
|
||||
});
|
||||
|
167
yarn.lock
167
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"
|
||||
|
Loading…
Reference in New Issue
Block a user