Implement user blocking on server side
authorChocobozzz <me@florianbigard.com>
Wed, 8 Aug 2018 12:58:21 +0000 (14:58 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 8 Aug 2018 13:22:58 +0000 (15:22 +0200)
15 files changed:
client/src/app/login/login.component.ts
package.json
server/controllers/api/users.ts
server/helpers/custom-validators/users.ts
server/initializers/constants.ts
server/initializers/migrations/0245-user-blocked.ts [new file with mode: 0644]
server/lib/oauth-model.ts
server/middlewares/oauth.ts
server/middlewares/validators/users.ts
server/models/account/user.ts
server/models/oauth/oauth-token.ts
server/tests/api/check-params/users.ts
server/tests/api/users/users.ts
server/tests/utils/users/users.ts
yarn.lock

index 56f992b5d605baa8ce342e86249cf3e6bc01d034..9a68c12faa4d358c74d01f86c9130c08d1de6b87 100644 (file)
@@ -55,7 +55,11 @@ export class LoginComponent extends FormReactive implements OnInit {
       .subscribe(
         () => this.redirectService.redirectToHomepage(),
 
-        err => this.error = err.message
+        err => {
+          if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.')
+          else if (err.message.indexOf('blocked') !== -1) this.error = this.i18n('You account is blocked.')
+          else this.error = err.message
+        }
       )
   }
 
index 6348bbb6ae7f58c4767b3ad9eb5ef8987be13fba..ba539bd313d1c8e86cee5ac85063e028958777bd 100644 (file)
@@ -78,6 +78,7 @@
     "@types/bluebird": "3.5.21"
   },
   "dependencies": {
+    "@types/oauth2-server": "^3.0.8",
     "async": "^2.0.0",
     "async-lock": "^1.1.2",
     "async-lru": "^1.1.1",
     "morgan": "^1.5.3",
     "multer": "^1.1.0",
     "nodemailer": "^4.4.2",
+    "oauth2-server": "^3.0.0",
     "parse-torrent": "^6.0.0",
     "password-generator": "^2.0.2",
     "pem": "^1.12.3",
index 3d2586c3a1f31bc8a21026f786f93a4256665d91..8f429d0b5f128089b18b5487ec578f1eaac53fb2 100644 (file)
@@ -32,6 +32,7 @@ import {
 import {
   deleteMeValidator,
   usersAskResetPasswordValidator,
+  usersBlockingValidator,
   usersResetPasswordValidator,
   videoImportsSortValidator,
   videosSortValidator
@@ -108,6 +109,19 @@ usersRouter.get('/',
   asyncMiddleware(listUsers)
 )
 
+usersRouter.post('/:id/block',
+  authenticate,
+  ensureUserHasRight(UserRight.MANAGE_USERS),
+  asyncMiddleware(usersBlockingValidator),
+  asyncMiddleware(blockUser)
+)
+usersRouter.post('/:id/unblock',
+  authenticate,
+  ensureUserHasRight(UserRight.MANAGE_USERS),
+  asyncMiddleware(usersBlockingValidator),
+  asyncMiddleware(unblockUser)
+)
+
 usersRouter.get('/:id',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_USERS),
@@ -278,6 +292,22 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
   return res.json(data)
 }
 
+async function unblockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const user: UserModel = res.locals.user
+
+  await changeUserBlock(res, user, false)
+
+  return res.status(204).end()
+}
+
+async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const user: UserModel = res.locals.user
+
+  await changeUserBlock(res, user, true)
+
+  return res.status(204).end()
+}
+
 function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
   return res.json((res.locals.user as UserModel).toFormattedJSON())
 }
@@ -423,3 +453,21 @@ async function resetUserPassword (req: express.Request, res: express.Response, n
 function success (req: express.Request, res: express.Response, next: express.NextFunction) {
   res.end()
 }
+
+async function changeUserBlock (res: express.Response, user: UserModel, block: boolean) {
+  const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
+
+  user.blocked = block
+
+  await sequelizeTypescript.transaction(async t => {
+    await OAuthTokenModel.deleteUserToken(user.id, t)
+
+    await user.save({ transaction: t })
+  })
+
+  auditLogger.update(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new UserAuditView(user.toFormattedJSON()),
+    oldUserAuditView
+  )
+}
index ce1323e947189b4f03c997afaf2e2bebed47bbe9..4a0d79ae58fff1c9fff770fbd61802cd198356f6 100644 (file)
@@ -2,7 +2,7 @@ import 'express-validator'
 import * as validator from 'validator'
 import { UserRole } from '../../../shared'
 import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers'
-import { exists, isFileValid } from './misc'
+import { exists, isFileValid, isBooleanValid } from './misc'
 import { values } from 'lodash'
 
 const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
@@ -29,17 +29,17 @@ function isUserDescriptionValid (value: string) {
   return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
 }
 
-function isBoolean (value: any) {
-  return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value))
-}
-
 const nsfwPolicies = values(NSFW_POLICY_TYPES)
 function isUserNSFWPolicyValid (value: any) {
   return exists(value) && nsfwPolicies.indexOf(value) !== -1
 }
 
 function isUserAutoPlayVideoValid (value: any) {
-  return isBoolean(value)
+  return isBooleanValid(value)
+}
+
+function isUserBlockedValid (value: any) {
+  return isBooleanValid(value)
 }
 
 function isUserRoleValid (value: any) {
@@ -57,6 +57,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
 // ---------------------------------------------------------------------------
 
 export {
+  isUserBlockedValid,
   isUserPasswordValid,
   isUserRoleValid,
   isUserVideoQuotaValid,
index 80eb3f1e7c887399e231e27982320ba3a2274109..0a651beed353958e7476160d89392ebac9b931c7 100644 (file)
@@ -15,7 +15,7 @@ let config: IConfig = require('config')
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 240
+const LAST_MIGRATION_VERSION = 245
 
 // ---------------------------------------------------------------------------
 
diff --git a/server/initializers/migrations/0245-user-blocked.ts b/server/initializers/migrations/0245-user-blocked.ts
new file mode 100644 (file)
index 0000000..67afea5
--- /dev/null
@@ -0,0 +1,40 @@
+import * as Sequelize from 'sequelize'
+import { createClient } from 'redis'
+import { CONFIG } from '../constants'
+import { JobQueue } from '../../lib/job-queue'
+import { initDatabaseModels } from '../database'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+}): Promise<any> {
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: true,
+      defaultValue: null
+    }
+    await utils.queryInterface.addColumn('user', 'blocked', data)
+  }
+
+  {
+    const query = 'UPDATE "user" SET "blocked" = false'
+    await utils.sequelize.query(query)
+  }
+
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: false,
+      defaultValue: null
+    }
+    await utils.queryInterface.changeColumn('user', 'blocked', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export { up, down }
index 3adcce7b0e1d4f25c01c784b2041c2340dcec364..f13c25795b841200f67c5630a795be8f7f5cc5be 100644 (file)
@@ -1,3 +1,4 @@
+import { AccessDeniedError} from 'oauth2-server'
 import { logger } from '../helpers/logger'
 import { UserModel } from '../models/account/user'
 import { OAuthClientModel } from '../models/oauth/oauth-client'
@@ -34,6 +35,8 @@ async function getUser (usernameOrEmail: string, password: string) {
   const passwordMatch = await user.isPasswordMatch(password)
   if (passwordMatch === false) return null
 
+  if (user.blocked) throw new AccessDeniedError('User is blocked.')
+
   return user
 }
 
@@ -67,9 +70,7 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
   }
 
   const tokenCreated = await OAuthTokenModel.create(tokenToCreate)
-  const tokenToReturn = Object.assign(tokenCreated, { client, user })
-
-  return tokenToReturn
+  return Object.assign(tokenCreated, { client, user })
 }
 
 // ---------------------------------------------------------------------------
index a6f28dd5b56a58050cc12d5f0b87aa2ad9e1e640..5233b66bd10ed29cfa5957a1b00169240497ceea 100644 (file)
@@ -39,7 +39,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF
     if (err) {
       return res.status(err.status)
         .json({
-          error: 'Authentication failed.',
+          error: err.message,
           code: err.name
         })
         .end()
index 3c207c81fa9b04e98beaa7c2aece996cc50c3bdb..94d8ab53bf8c6f622727e272b3bfed6275152214 100644 (file)
@@ -74,6 +74,26 @@ const usersRemoveValidator = [
   }
 ]
 
+const usersBlockingValidator = [
+  param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking usersRemove parameters', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+    if (!await checkUserIdExist(req.params.id, res)) return
+
+    const user = res.locals.user
+    if (user.username === 'root') {
+      return res.status(400)
+                .send({ error: 'Cannot block the root user' })
+                .end()
+    }
+
+    return next()
+  }
+]
+
 const deleteMeValidator = [
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     const user: UserModel = res.locals.oauth.token.User
@@ -230,6 +250,7 @@ export {
   usersAddValidator,
   deleteMeValidator,
   usersRegisterValidator,
+  usersBlockingValidator,
   usersRemoveValidator,
   usersUpdateValidator,
   usersUpdateMeValidator,
index 1b1fc5ee80197a3d558b02320bc9b814c055f129..ea6d63312314fcdc264c72da1c3358465ef89c54 100644 (file)
@@ -21,6 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
 import { User, UserRole } from '../../../shared/models/users'
 import {
   isUserAutoPlayVideoValid,
+  isUserBlockedValid,
   isUserNSFWPolicyValid,
   isUserPasswordValid,
   isUserRoleValid,
@@ -100,6 +101,12 @@ export class UserModel extends Model<UserModel> {
   @Column
   autoPlayVideo: boolean
 
+  @AllowNull(false)
+  @Default(false)
+  @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
+  @Column
+  blocked: boolean
+
   @AllowNull(false)
   @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
   @Column
index 026c301359d9b47e1b477bac2c97013aeb623627..4c53848dc4aa77220659d436d0b55ad12910b8e4 100644 (file)
@@ -3,6 +3,7 @@ import { logger } from '../../helpers/logger'
 import { AccountModel } from '../account/account'
 import { UserModel } from '../account/user'
 import { OAuthClientModel } from './oauth-client'
+import { Transaction } from 'sequelize'
 
 export type OAuthTokenInfo = {
   refreshToken: string
@@ -125,7 +126,7 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
         } as OAuthTokenInfo
       })
       .catch(err => {
-        logger.info('getRefreshToken error.', { err })
+        logger.error('getRefreshToken error.', { err })
         throw err
       })
   }
@@ -163,11 +164,12 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
       })
   }
 
-  static deleteUserToken (userId: number) {
+  static deleteUserToken (userId: number, t?: Transaction) {
     const query = {
       where: {
         userId
-      }
+      },
+      transaction: t
     }
 
     return OAuthTokenModel.destroy(query)
index 60165ae228421de5e2e9b17f3ab2e8757a335346..b3fb61f6ce704d7e4ff4b2e74e027ae5f1742c24 100644 (file)
@@ -8,7 +8,7 @@ import { UserRole, VideoImport, VideoImportState } from '../../../../shared'
 import {
   createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest,
   makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers,
-  updateUser, uploadVideo, userLogin, deleteMe
+  updateUser, uploadVideo, userLogin, deleteMe, unblockUser, blockUser
 } from '../../utils'
 import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
 import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../utils/videos/video-imports'
@@ -455,17 +455,29 @@ describe('Test users API validators', function () {
     })
   })
 
-  describe('When removing an user', function () {
+  describe('When blocking/unblocking/removing user', function () {
     it('Should fail with an incorrect id', async function () {
       await removeUser(server.url, 'blabla', server.accessToken, 400)
+      await blockUser(server.url, 'blabla', server.accessToken, 400)
+      await unblockUser(server.url, 'blabla', server.accessToken, 400)
     })
 
     it('Should fail with the root user', async function () {
       await removeUser(server.url, rootId, server.accessToken, 400)
+      await blockUser(server.url, rootId, server.accessToken, 400)
+      await unblockUser(server.url, rootId, server.accessToken, 400)
     })
 
     it('Should return 404 with a non existing id', async function () {
       await removeUser(server.url, 4545454, server.accessToken, 404)
+      await blockUser(server.url, 4545454, server.accessToken, 404)
+      await unblockUser(server.url, 4545454, server.accessToken, 404)
+    })
+
+    it('Should fail with a non admin user', async function () {
+      await removeUser(server.url, userId, userAccessToken, 403)
+      await blockUser(server.url, userId, userAccessToken, 403)
+      await unblockUser(server.url, userId, userAccessToken, 403)
     })
   })
 
index c9e8eb6f9310bd77ace07daa6c0bfea1e030475c..77aa00f60860ed71e0b69c435e767d11bdf546a3 100644 (file)
@@ -7,7 +7,7 @@ import {
   createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating,
   getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo,
   registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin,
-  deleteMe
+  deleteMe, blockUser, unblockUser
 } from '../../utils/index'
 import { follow } from '../../utils/server/follows'
 import { setAccessTokensToServers } from '../../utils/users/login'
@@ -45,28 +45,28 @@ describe('Test users', function () {
     const client = { id: 'client', secret: server.client.secret }
     const res = await login(server.url, client, server.user, 400)
 
-    expect(res.body.error).to.equal('Authentication failed.')
+    expect(res.body.error).to.contain('client is invalid')
   })
 
   it('Should not login with an invalid client secret', async function () {
     const client = { id: server.client.id, secret: 'coucou' }
     const res = await login(server.url, client, server.user, 400)
 
-    expect(res.body.error).to.equal('Authentication failed.')
+    expect(res.body.error).to.contain('client is invalid')
   })
 
   it('Should not login with an invalid username', async function () {
     const user = { username: 'captain crochet', password: server.user.password }
     const res = await login(server.url, server.client, user, 400)
 
-    expect(res.body.error).to.equal('Authentication failed.')
+    expect(res.body.error).to.contain('credentials are invalid')
   })
 
   it('Should not login with an invalid password', async function () {
     const user = { username: server.user.username, password: 'mew_three' }
     const res = await login(server.url, server.client, user, 400)
 
-    expect(res.body.error).to.equal('Authentication failed.')
+    expect(res.body.error).to.contain('credentials are invalid')
   })
 
   it('Should not be able to upload a video', async function () {
@@ -493,6 +493,27 @@ describe('Test users', function () {
     }
   })
 
+  it('Should block and unblock a user', async function () {
+    const user16 = {
+      username: 'user_16',
+      password: 'my super password'
+    }
+    const resUser = await createUser(server.url, server.accessToken, user16.username, user16.password)
+    const user16Id = resUser.body.user.id
+
+    accessToken = await userLogin(server, user16)
+
+    await getMyUserInformation(server.url, accessToken, 200)
+    await blockUser(server.url, user16Id, server.accessToken)
+
+    await getMyUserInformation(server.url, accessToken, 401)
+    await userLogin(server, user16, 400)
+
+    await unblockUser(server.url, user16Id, server.accessToken)
+    accessToken = await userLogin(server, user16)
+    await getMyUserInformation(server.url, accessToken, 200)
+  })
+
   after(async function () {
     killallServers([ server ])
 
index e24e721bdba5604f240c3abef2b10f62fb22b115..7e15fc86ec012d46f092df132242f097a76f19c5 100644 (file)
@@ -134,6 +134,26 @@ function removeUser (url: string, userId: number | string, accessToken: string,
           .expect(expectedStatus)
 }
 
+function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
+  const path = '/api/v1/users'
+
+  return request(url)
+    .post(path + '/' + userId + '/block')
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(expectedStatus)
+}
+
+function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
+  const path = '/api/v1/users'
+
+  return request(url)
+    .post(path + '/' + userId + '/unblock')
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(expectedStatus)
+}
+
 function updateMyUser (options: {
   url: string
   accessToken: string,
@@ -234,6 +254,8 @@ export {
   updateUser,
   updateMyUser,
   getUserInformation,
+  blockUser,
+  unblockUser,
   askResetPassword,
   resetPassword,
   updateMyAvatar
index 206700a87e7ba1c955f005491dc63ed28e788ed8..c1a3d6e8846557a589aa4d16ce831c1b5350b58c 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@types/events" "*"
     "@types/node" "*"
 
+"@types/oauth2-server@^3.0.8":
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/@types/oauth2-server/-/oauth2-server-3.0.8.tgz#0b7f5083790732ea00bf8c5e0b04b9fa1f22f22c"
+  dependencies:
+    "@types/express" "*"
+
 "@types/parse-torrent-file@*":
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.1.tgz#056a6c18f3fac0cd7c6c74540f00496a3225976b"
@@ -2713,7 +2719,15 @@ fsevents@^1.2.2:
     nan "^2.9.2"
     node-pre-gyp "^0.10.0"
 
-fstream@^1.0.0, fstream@^1.0.2:
+fstream-ignore@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+  dependencies:
+    fstream "^1.0.0"
+    inherits "2"
+    minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
   dependencies:
@@ -3227,7 +3241,7 @@ hashish@~0.0.4:
   dependencies:
     traverse ">=0.2.4"
 
-hawk@~3.1.3:
+hawk@3.1.3, hawk@~3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
   dependencies:
@@ -4115,13 +4129,13 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
-libxmljs@0.19.1:
-  version "0.19.1"
-  resolved "https://registry.yarnpkg.com/libxmljs/-/libxmljs-0.19.1.tgz#bc7a62822c4392363feaab49b116b4786b2d5ada"
+libxmljs@0.19.0:
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/libxmljs/-/libxmljs-0.19.0.tgz#dd0e635ce752af7701492ceb8c565ab74d494473"
   dependencies:
     bindings "~1.3.0"
     nan "~2.10.0"
-    node-pre-gyp "~0.10.2"
+    node-pre-gyp "~0.6.37"
 
 lint-staged@^7.1.0:
   version "7.2.0"
@@ -4701,7 +4715,7 @@ minimalistic-assert@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
 
-"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
@@ -4959,7 +4973,7 @@ node-pre-gyp@0.10.2:
     semver "^5.3.0"
     tar "^4"
 
-node-pre-gyp@^0.10.0, node-pre-gyp@~0.10.2:
+node-pre-gyp@^0.10.0:
   version "0.10.3"
   resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
   dependencies:
@@ -4974,6 +4988,22 @@ node-pre-gyp@^0.10.0, node-pre-gyp@~0.10.2:
     semver "^5.3.0"
     tar "^4"
 
+node-pre-gyp@~0.6.37:
+  version "0.6.39"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+  dependencies:
+    detect-libc "^1.0.2"
+    hawk "3.1.3"
+    mkdirp "^0.5.1"
+    nopt "^4.0.1"
+    npmlog "^4.0.2"
+    rc "^1.1.7"
+    request "2.81.0"
+    rimraf "^2.6.1"
+    semver "^5.3.0"
+    tar "^2.2.1"
+    tar-pack "^3.4.0"
+
 node-sass@^4.9.0:
   version "4.9.2"
   resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.2.tgz#5e63fe6bd0f2ae3ac9d6c14ede8620e2b8bdb437"
@@ -5133,7 +5163,7 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 
-oauth2-server@3.0.0:
+oauth2-server@3.0.0, oauth2-server@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.0.0.tgz#c46276b74c3d28634d59ee981f76b58a6459cc28"
   dependencies:
@@ -5838,7 +5868,7 @@ raw-body@~1.1.0:
     bytes "1"
     string_decoder "0.10"
 
-rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
+rc@^1.0.1, rc@^1.1.6, rc@^1.1.7, rc@^1.2.7:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
   dependencies:
@@ -6044,32 +6074,7 @@ repeating@^2.0.0:
   dependencies:
     is-finite "^1.0.0"
 
-request@2.87.0, request@^2.81.0, request@^2.83.0:
-  version "2.87.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.6.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.5"
-    extend "~3.0.1"
-    forever-agent "~0.6.1"
-    form-data "~2.3.1"
-    har-validator "~5.0.3"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.17"
-    oauth-sign "~0.8.2"
-    performance-now "^2.1.0"
-    qs "~6.5.1"
-    safe-buffer "^5.1.1"
-    tough-cookie "~2.3.3"
-    tunnel-agent "^0.6.0"
-    uuid "^3.1.0"
-
-"request@>=2.9.0 <2.82.0":
+request@2.81.0, "request@>=2.9.0 <2.82.0":
   version "2.81.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
   dependencies:
@@ -6096,6 +6101,31 @@ request@2.87.0, request@^2.81.0, request@^2.83.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
+request@2.87.0, request@^2.81.0, request@^2.83.0:
+  version "2.87.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.6.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.5"
+    extend "~3.0.1"
+    forever-agent "~0.6.1"
+    form-data "~2.3.1"
+    har-validator "~5.0.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.17"
+    oauth-sign "~0.8.2"
+    performance-now "^2.1.0"
+    qs "~6.5.1"
+    safe-buffer "^5.1.1"
+    tough-cookie "~2.3.3"
+    tunnel-agent "^0.6.0"
+    uuid "^3.1.0"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -7081,6 +7111,19 @@ tar-fs@^1.13.0:
     pump "^1.0.0"
     tar-stream "^1.1.2"
 
+tar-pack@^3.4.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
+  dependencies:
+    debug "^2.2.0"
+    fstream "^1.0.10"
+    fstream-ignore "^1.0.5"
+    once "^1.3.3"
+    readable-stream "^2.1.4"
+    rimraf "^2.5.1"
+    tar "^2.2.1"
+    uid-number "^0.0.6"
+
 tar-stream@^1.1.2:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.1.tgz#f84ef1696269d6223ca48f6e1eeede3f7e81f395"
@@ -7093,7 +7136,7 @@ tar-stream@^1.1.2:
     to-buffer "^1.1.0"
     xtend "^4.0.0"
 
-tar@^2.0.0:
+tar@^2.0.0, tar@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
   dependencies:
@@ -7454,6 +7497,10 @@ uglify-to-browserify@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
 
+uid-number@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
 uint64be@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-1.0.1.tgz#1f7154202f2a1b8af353871dda651bf34ce93e95"