Support logout and add id and pass tests
authorChocobozzz <me@florianbigard.com>
Thu, 23 Apr 2020 09:36:50 +0000 (11:36 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 4 May 2020 14:21:39 +0000 (16:21 +0200)
25 files changed:
scripts/update-host.ts
server/controllers/api/accounts.ts
server/controllers/api/server/follows.ts
server/controllers/api/server/server-blocklist.ts
server/controllers/api/users/index.ts
server/controllers/api/users/token.ts [new file with mode: 0644]
server/controllers/api/video-channel.ts
server/controllers/api/video-playlist.ts
server/lib/auth.ts
server/lib/job-queue/job-queue.ts
server/lib/oauth-model.ts
server/lib/plugins/plugin-manager.ts
server/lib/plugins/register-helpers-store.ts
server/middlewares/oauth.ts
server/middlewares/validators/themes.ts
server/models/account/user.ts
server/models/oauth/oauth-token.ts
server/tests/api/users/users.ts
server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js
server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js
server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
server/tests/plugins/id-and-pass-auth.ts
server/typings/express.ts
server/typings/plugins/register-server-option.model.ts
shared/models/plugins/register-server-auth.model.ts

index 54b31d7865814267ed0fcb85798280f0d40fa7cb..7b07dea04936f814f28d2d6478e13ca816d54318 100755 (executable)
@@ -11,15 +11,15 @@ import {
   getVideoAnnounceActivityPubUrl,
   getVideoChannelActivityPubUrl,
   getVideoCommentActivityPubUrl
-} from '../server/lib/activitypub'
+} from '../server/lib/activitypub/url'
 import { VideoShareModel } from '../server/models/video/video-share'
 import { VideoCommentModel } from '../server/models/video/video-comment'
-import { getServerActor } from '../server/helpers/utils'
 import { AccountModel } from '../server/models/account/account'
 import { VideoChannelModel } from '../server/models/video/video-channel'
 import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist'
 import { initDatabaseModels } from '../server/initializers'
 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
+import { getServerActor } from '@server/models/application/application'
 
 run()
   .then(() => process.exit(0))
index 3bbb0a43e943d3d42b3a7182b4ef6ce453df8ccf..ccdc610a2de569e8b2f9358cb339a7038471cb6d 100644 (file)
@@ -1,5 +1,5 @@
 import * as express from 'express'
-import { getFormattedObjects} from '../../helpers/utils'
+import { getFormattedObjects } from '../../helpers/utils'
 import {
   asyncMiddleware,
   authenticate,
index 82e9ef898c329aa8aaa8a605c08c55c334832398..23823c9fb2f2aa1a0577b82ed6b113f7962b8f41 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { UserRight } from '../../../../shared/models/users'
 import { logger } from '../../../helpers/logger'
-import { getFormattedObjects} from '../../../helpers/utils'
+import { getFormattedObjects } from '../../../helpers/utils'
 import { SERVER_ACTOR_NAME } from '../../../initializers/constants'
 import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
 import {
index 008b8d4ea0d70f33b8343efd8ba2bcdc070b1949..f849b15c76e7a86c4628164d614de2696c5bfddd 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import 'multer'
-import { getFormattedObjects} from '../../../helpers/utils'
+import { getFormattedObjects } from '../../../helpers/utils'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
index b30f42b4336766c8e5c8b88778349febfc8d69e4..c488f720b83981e4840d5ea41fe6c0392d77b43d 100644 (file)
@@ -26,12 +26,12 @@ import {
   usersUpdateValidator
 } from '../../../middlewares'
 import {
+  ensureCanManageUser,
   usersAskResetPasswordValidator,
   usersAskSendVerifyEmailValidator,
   usersBlockingValidator,
   usersResetPasswordValidator,
-  usersVerifyEmailValidator,
-  ensureCanManageUser
+  usersVerifyEmailValidator
 } from '../../../middlewares/validators'
 import { UserModel } from '../../../models/account/user'
 import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -49,15 +49,10 @@ import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
 import { UserRegister } from '../../../../shared/models/users/user-register.model'
 import { MUser, MUserAccountDefault } from '@server/typings/models'
 import { Hooks } from '@server/lib/plugins/hooks'
-import { handleIdAndPassLogin } from '@server/lib/auth'
+import { tokensRouter } from '@server/controllers/api/users/token'
 
 const auditLogger = auditLoggerFactory('users')
 
-const loginRateLimiter = RateLimit({
-  windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
-  max: CONFIG.RATES_LIMIT.LOGIN.MAX
-})
-
 // @ts-ignore
 const signupRateLimiter = RateLimit({
   windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
@@ -72,6 +67,7 @@ const askSendEmailLimiter = new RateLimit({
 })
 
 const usersRouter = express.Router()
+usersRouter.use('/', tokensRouter)
 usersRouter.use('/', myNotificationsRouter)
 usersRouter.use('/', mySubscriptionsRouter)
 usersRouter.use('/', myBlocklistRouter)
@@ -168,23 +164,6 @@ usersRouter.post('/:id/verify-email',
   asyncMiddleware(verifyUserEmail)
 )
 
-usersRouter.post('/token',
-  loginRateLimiter,
-  handleIdAndPassLogin,
-  tokenSuccess
-)
-usersRouter.post('/token',
-  loginRateLimiter,
-  handleIdAndPassLogin,
-  tokenSuccess
-)
-usersRouter.post('/revoke-token',
-  loginRateLimiter,
-  handleIdAndPassLogin,
-  tokenSuccess
-)
-// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route
-
 // ---------------------------------------------------------------------------
 
 export {
@@ -391,12 +370,6 @@ async function verifyUserEmail (req: express.Request, res: express.Response) {
   return res.status(204).end()
 }
 
-function tokenSuccess (req: express.Request) {
-  const username = req.body.username
-
-  Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
-}
-
 async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
   const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
 
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
new file mode 100644 (file)
index 0000000..9694f9e
--- /dev/null
@@ -0,0 +1,38 @@
+import { handleIdAndPassLogin, handleTokenRevocation } from '@server/lib/auth'
+import * as RateLimit from 'express-rate-limit'
+import { CONFIG } from '@server/initializers/config'
+import * as express from 'express'
+import { Hooks } from '@server/lib/plugins/hooks'
+import { asyncMiddleware, authenticate } from '@server/middlewares'
+
+const tokensRouter = express.Router()
+
+const loginRateLimiter = RateLimit({
+  windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
+  max: CONFIG.RATES_LIMIT.LOGIN.MAX
+})
+
+tokensRouter.post('/token',
+  loginRateLimiter,
+  handleIdAndPassLogin,
+  tokenSuccess
+)
+
+tokensRouter.post('/revoke-token',
+  authenticate,
+  asyncMiddleware(handleTokenRevocation),
+  tokenSuccess
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  tokensRouter
+}
+// ---------------------------------------------------------------------------
+
+function tokenSuccess (req: express.Request) {
+  const username = req.body.username
+
+  Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
+}
index faef5ba4b0309bfe7f92d7ee6dee6e21cae54987..d779f1aab5b187065a75a8459de01f40b6f2523a 100644 (file)
@@ -1,5 +1,5 @@
 import * as express from 'express'
-import { getFormattedObjects} from '../../helpers/utils'
+import { getFormattedObjects } from '../../helpers/utils'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
index 49ac3c80e410a2f1e5b635333699383420b610e7..375d711fdcde9f1e04998a7a2cba547ead17437e 100644 (file)
@@ -1,5 +1,5 @@
 import * as express from 'express'
-import { getFormattedObjects} from '../../helpers/utils'
+import { getFormattedObjects } from '../../helpers/utils'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
index 18d52fa5a7ff5f5623f9a2dbf43a0f03007ed03e..3495571dbcbf42ba57a891b9d488fb5425050b51 100644 (file)
@@ -5,6 +5,7 @@ import { PluginManager } from '@server/lib/plugins/plugin-manager'
 import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model'
 import { logger } from '@server/helpers/logger'
 import { UserRole } from '@shared/models'
+import { revokeToken } from '@server/lib/oauth-model'
 
 const oAuthServer = new OAuthServer({
   useErrorHandler: true,
@@ -37,8 +38,9 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response
     const aWeight = a.registerAuthOptions.getWeight()
     const bWeight = b.registerAuthOptions.getWeight()
 
+    // DESC weight order
     if (aWeight === bWeight) return 0
-    if (aWeight > bWeight) return 1
+    if (aWeight < bWeight) return 1
     return -1
   })
 
@@ -48,18 +50,24 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response
   }
 
   for (const pluginAuth of pluginAuths) {
+    const authOptions = pluginAuth.registerAuthOptions
+
     logger.debug(
-      'Using auth method of %s to login %s with weight %d.',
-      pluginAuth.npmName, loginOptions.id, pluginAuth.registerAuthOptions.getWeight()
+      'Using auth method %s of plugin %s to login %s with weight %d.',
+      authOptions.authName, pluginAuth.npmName, loginOptions.id, authOptions.getWeight()
     )
 
-    const loginResult = await pluginAuth.registerAuthOptions.login(loginOptions)
+    const loginResult = await authOptions.login(loginOptions)
     if (loginResult) {
-      logger.info('Login success with plugin %s for %s.', pluginAuth.npmName, loginOptions.id)
+      logger.info(
+        'Login success with auth method %s of plugin %s for %s.',
+        authOptions.authName, pluginAuth.npmName, loginOptions.id
+      )
 
       res.locals.bypassLogin = {
         bypass: true,
         pluginName: pluginAuth.npmName,
+        authName: authOptions.authName,
         user: {
           username: loginResult.username,
           email: loginResult.email,
@@ -75,12 +83,40 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response
   return localLogin(req, res, next)
 }
 
+async function handleTokenRevocation (req: express.Request, res: express.Response) {
+  const token = res.locals.oauth.token
+
+  PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName)
+
+  await revokeToken(token)
+    .catch(err => {
+      logger.error('Cannot revoke token.', err)
+    })
+
+  // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
+  // oAuthServer.revoke(req, res, err => {
+  //   if (err) {
+  //     logger.warn('Error in revoke token handler.', { err })
+  //
+  //     return res.status(err.status)
+  //               .json({
+  //                 error: err.message,
+  //                 code: err.name
+  //               })
+  //               .end()
+  //   }
+  // })
+
+  return res.sendStatus(200)
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   oAuthServer,
   handleIdAndPassLogin,
-  onExternalAuthPlugin
+  onExternalAuthPlugin,
+  handleTokenRevocation
 }
 
 // ---------------------------------------------------------------------------
@@ -88,6 +124,8 @@ export {
 function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
   return oAuthServer.token()(req, res, err => {
     if (err) {
+      logger.warn('Login error.', { err })
+
       return res.status(err.status)
                 .json({
                   error: err.message,
index d8d64caaf348bae8e464cfb58bbd1d53f26e46dd..14e181835a381bf8cde10dffcf5091f297bcb471 100644 (file)
@@ -2,9 +2,16 @@ import * as Bull from 'bull'
 import {
   ActivitypubFollowPayload,
   ActivitypubHttpBroadcastPayload,
-  ActivitypubHttpFetcherPayload, ActivitypubHttpUnicastPayload, EmailPayload,
+  ActivitypubHttpFetcherPayload,
+  ActivitypubHttpUnicastPayload,
+  EmailPayload,
   JobState,
-  JobType, RefreshPayload, VideoFileImportPayload, VideoImportPayload, VideoRedundancyPayload, VideoTranscodingPayload
+  JobType,
+  RefreshPayload,
+  VideoFileImportPayload,
+  VideoImportPayload,
+  VideoRedundancyPayload,
+  VideoTranscodingPayload
 } from '../../../shared/models'
 import { logger } from '../../helpers/logger'
 import { Redis } from '../redis'
@@ -13,13 +20,13 @@ import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-bro
 import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher'
 import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast'
 import { processEmail } from './handlers/email'
-import { processVideoTranscoding} from './handlers/video-transcoding'
+import { processVideoTranscoding } from './handlers/video-transcoding'
 import { processActivityPubFollow } from './handlers/activitypub-follow'
-import { processVideoImport} from './handlers/video-import'
+import { processVideoImport } from './handlers/video-import'
 import { processVideosViews } from './handlers/video-views'
-import { refreshAPObject} from './handlers/activitypub-refresher'
-import { processVideoFileImport} from './handlers/video-file-import'
-import { processVideoRedundancy} from '@server/lib/job-queue/handlers/video-redundancy'
+import { refreshAPObject } from './handlers/activitypub-refresher'
+import { processVideoFileImport } from './handlers/video-file-import'
+import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy'
 
 type CreateJobArgument =
   { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -117,7 +124,7 @@ class JobQueue {
 
   createJob (obj: CreateJobArgument): void {
     this.createJobWithPromise(obj)
-         .catch(err => logger.error('Cannot create job.', { err, obj }))
+        .catch(err => logger.error('Cannot create job.', { err, obj }))
   }
 
   createJobWithPromise (obj: CreateJobArgument) {
index ea4a678023cd4e381ddf0e1e31f18a267735426d..7a6ed63be3477d78aaefeef7b74995dc268f48a0 100644 (file)
@@ -14,6 +14,7 @@ import { MUser } from '@server/typings/models/user/user'
 import { UserAdminFlag } from '@shared/models/users/user-flag.model'
 import { createUserAccountAndChannelAndPlaylist } from './user'
 import { UserRole } from '@shared/models/users/user-role'
+import { PluginManager } from '@server/lib/plugins/plugin-manager'
 
 type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
 
@@ -82,7 +83,7 @@ async function getUser (usernameOrEmail: string, password: string) {
     const obj = res.locals.bypassLogin
     logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
 
-    let user = await UserModel.loadByEmail(obj.user.username)
+    let user = await UserModel.loadByEmail(obj.user.email)
     if (!user) user = await createUserFromExternal(obj.pluginName, obj.user)
 
     // This user does not belong to this plugin, skip it
@@ -94,7 +95,8 @@ async function getUser (usernameOrEmail: string, password: string) {
   logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).')
 
   const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail)
-  if (!user) return null
+  // If we don't find the user, or if the user belongs to a plugin
+  if (!user || user.pluginAuth !== null) return null
 
   const passwordMatch = await user.isPasswordMatch(password)
   if (passwordMatch === false) return null
@@ -109,8 +111,14 @@ async function getUser (usernameOrEmail: string, password: string) {
 }
 
 async function revokeToken (tokenInfo: TokenInfo) {
+  const res: express.Response = this.request.res
   const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
+
   if (token) {
+    if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) {
+      PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName)
+    }
+
     clearCacheByToken(token.accessToken)
 
     token.destroy()
@@ -123,6 +131,12 @@ async function revokeToken (tokenInfo: TokenInfo) {
 }
 
 async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
+  const res: express.Response = this.request.res
+
+  const authName = res.locals.bypassLogin?.bypass === true
+    ? res.locals.bypassLogin.authName
+    : null
+
   logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
 
   const tokenToCreate = {
@@ -130,6 +144,7 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
     accessTokenExpiresAt: token.accessTokenExpiresAt,
     refreshToken: token.refreshToken,
     refreshTokenExpiresAt: token.refreshTokenExpiresAt,
+    authName,
     oAuthClientId: client.id,
     userId: user.id
   }
index f78b989f510fa0592574dd013a61997665b45c21..9d646b68976557da9f9d562a6c405be9e06438d9 100644 (file)
@@ -76,7 +76,7 @@ export class PluginManager implements ServerHook {
     return this.registeredPlugins[npmName]
   }
 
-  getRegisteredPlugin (name: string) {
+  getRegisteredPluginByShortName (name: string) {
     const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN)
     const registered = this.getRegisteredPluginOrTheme(npmName)
 
@@ -85,7 +85,7 @@ export class PluginManager implements ServerHook {
     return registered
   }
 
-  getRegisteredTheme (name: string) {
+  getRegisteredThemeByShortName (name: string) {
     const npmName = PluginModel.buildNpmName(name, PluginType.THEME)
     const registered = this.getRegisteredPluginOrTheme(npmName)
 
@@ -132,6 +132,22 @@ export class PluginManager implements ServerHook {
     return this.translations[locale] || {}
   }
 
+  onLogout (npmName: string, authName: string) {
+    const plugin = this.getRegisteredPluginOrTheme(npmName)
+    if (!plugin || plugin.type !== PluginType.PLUGIN) return
+
+    const auth = plugin.registerHelpersStore.getIdAndPassAuths()
+      .find(a => a.authName === authName)
+
+    if (auth.onLogout) {
+      try {
+        auth.onLogout()
+      } catch (err) {
+        logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err })
+      }
+    }
+  }
+
   // ###################### Hooks ######################
 
   async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
index 7e827401f580e43c064f9e1e64149cbcf87b9796..679ed365029f66d138e0ed9095d73a24f9dc6f7f 100644 (file)
@@ -171,6 +171,11 @@ export class RegisterHelpersStore {
 
   private buildRegisterIdAndPassAuth () {
     return (options: RegisterServerAuthPassOptions) => {
+      if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') {
+        logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName)
+        return
+      }
+
       this.idAndPassAuths.push(options)
     }
   }
index 4ae7f18c2002e1182964c0c04fde29cfd5635cee..9d0eaa51f277dbbbbb60cb52ac06145eb90e3500 100644 (file)
@@ -2,7 +2,7 @@ import * as express from 'express'
 import { logger } from '../helpers/logger'
 import { Socket } from 'socket.io'
 import { getAccessToken } from '../lib/oauth-model'
-import { handleIdAndPassLogin, oAuthServer } from '@server/lib/auth'
+import { oAuthServer } from '@server/lib/auth'
 
 function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) {
   const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {}
index 24a9673f788d5c08194a3cecf81e518b5d77626a..82794656d5553265740c145c111c2899e6d948e5 100644 (file)
@@ -16,7 +16,7 @@ const serveThemeCSSValidator = [
 
     if (areValidationErrors(req, res)) return
 
-    const theme = PluginManager.Instance.getRegisteredTheme(req.params.themeName)
+    const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
 
     if (!theme || theme.version !== req.params.themeVersion) {
       return res.sendStatus(404)
index d0d9a0508b379d4393f2c0e1a96222825b19ffeb..1bff955df06306f2d6c22a385b1d7df53a57d183 100644 (file)
@@ -222,7 +222,7 @@ enum ScopeNames {
 export class UserModel extends Model<UserModel> {
 
   @AllowNull(true)
-  @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
+  @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
   @Column
   password: string
 
@@ -388,7 +388,7 @@ export class UserModel extends Model<UserModel> {
   @BeforeCreate
   @BeforeUpdate
   static cryptPasswordIfNeeded (instance: UserModel) {
-    if (instance.changed('password')) {
+    if (instance.changed('password') && instance.password) {
       return cryptPassword(instance.password)
         .then(hash => {
           instance.password = hash
index d2101ce86f9cb4ddfa668890537cc7ac313357f3..e73c4be7d54a8496e09350448bb665cad639f306 100644 (file)
@@ -97,6 +97,9 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
   @Column
   refreshTokenExpiresAt: Date
 
+  @Column
+  authName: string
+
   @CreatedAt
   createdAt: Date
 
index 7ba04a4cad659fd5a3ad83bd41977acaf5ba2037..60fbd2a20a6b7672b5ba7ddec602450aa2c1ebfd 100644 (file)
@@ -2,8 +2,9 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { MyUser, User, UserRole, Video, VideoPlaylistType, VideoAbuseState, VideoAbuseUpdate } from '../../../../shared/index'
+import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index'
 import {
+  addVideoCommentThread,
   blockUser,
   cleanupTests,
   createUser,
@@ -11,12 +12,14 @@ import {
   flushAndRunServer,
   getAccountRatings,
   getBlacklistedVideosList,
+  getCustomConfig,
   getMyUserInformation,
   getMyUserVideoQuotaUsed,
   getMyUserVideoRating,
   getUserInformation,
   getUsersList,
   getUsersListPaginationAndSort,
+  getVideoAbusesList,
   getVideoChannel,
   getVideosList,
   installPlugin,
@@ -26,21 +29,21 @@ import {
   registerUserWithChannel,
   removeUser,
   removeVideo,
+  reportVideoAbuse,
   ServerInfo,
   testImage,
   unblockUser,
+  updateCustomSubConfig,
   updateMyAvatar,
   updateMyUser,
   updateUser,
+  updateVideoAbuse,
   uploadVideo,
   userLogin,
-  reportVideoAbuse,
-  addVideoCommentThread,
-  updateVideoAbuse,
-  getVideoAbusesList, updateCustomSubConfig, getCustomConfig, waitJobs
+  waitJobs
 } from '../../../../shared/extra-utils'
 import { follow } from '../../../../shared/extra-utils/server/follows'
-import { setAccessTokensToServers, logout } from '../../../../shared/extra-utils/users/login'
+import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
 import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
 import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
 import { CustomConfig } from '@shared/models/server'
@@ -60,7 +63,14 @@ describe('Test users', function () {
 
   before(async function () {
     this.timeout(30000)
-    server = await flushAndRunServer(1)
+
+    server = await flushAndRunServer(1, {
+      rates_limit: {
+        login: {
+          max: 30
+        }
+      }
+    })
 
     await setAccessTokensToServers([ server ])
 
@@ -217,8 +227,6 @@ describe('Test users', function () {
       await uploadVideo(server.url, server.accessToken, { name: 'video' }, 401)
     })
 
-    it('Should not be able to remove a video')
-
     it('Should not be able to rate a video', async function () {
       const path = '/api/v1/videos/'
       const data = {
@@ -235,13 +243,17 @@ describe('Test users', function () {
       await makePutBodyRequest(options)
     })
 
-    it('Should be able to login again')
+    it('Should be able to login again', async function () {
+      server.accessToken = await serverLogin(server)
+    })
 
     it('Should have an expired access token')
 
     it('Should refresh the token')
 
-    it('Should be able to upload a video again')
+    it('Should be able to get my user information again', async function () {
+      await getMyUserInformation(server.url, server.accessToken)
+    })
   })
 
   describe('Creating a user', function () {
index 4755ed643c9d8210b567972a2e4c18185e11ebe6..9fc12a3e33c59bc7305ee7d720378738cf1a598f 100644 (file)
@@ -3,7 +3,7 @@ async function register ({
   peertubeHelpers
 }) {
   registerIdAndPassAuth({
-    type: 'id-and-pass',
+    authName: 'spyro-auth',
 
     onLogout: () => {
       peertubeHelpers.logger.info('On logout for auth 1 - 1')
@@ -16,7 +16,7 @@ async function register ({
         return Promise.resolve({
           username: 'spyro',
           email: 'spyro@example.com',
-          role: 0,
+          role: 2,
           displayName: 'Spyro the Dragon'
         })
       }
@@ -26,7 +26,7 @@ async function register ({
   })
 
   registerIdAndPassAuth({
-    type: 'id-and-pass',
+    authName: 'crash-auth',
 
     onLogout: () => {
       peertubeHelpers.logger.info('On logout for auth 1 - 2')
@@ -39,7 +39,7 @@ async function register ({
         return Promise.resolve({
           username: 'crash',
           email: 'crash@example.com',
-          role: 2,
+          role: 1,
           displayName: 'Crash Bandicoot'
         })
       }
index 2a15b3754fb4c5c49c806065d6310c5475ecaa68..372f3fa0c1c0f11aa06baa2edd077df824a46065 100644 (file)
@@ -3,7 +3,7 @@ async function register ({
   peertubeHelpers
 }) {
   registerIdAndPassAuth({
-    type: 'id-and-pass',
+    authName: 'laguna-bad-auth',
 
     onLogout: () => {
       peertubeHelpers.logger.info('On logout for auth 3 - 1')
index edfc870c026780e85eb98d5cdabfe72ecf1d3561..c0e560019c59e5951d25a4457b39057126e472a5 100644 (file)
@@ -3,7 +3,7 @@ async function register ({
   peertubeHelpers
 }) {
   registerIdAndPassAuth({
-    type: 'id-and-pass',
+    authName: 'laguna-auth',
 
     onLogout: () => {
       peertubeHelpers.logger.info('On logout for auth 2 - 1')
index 5b4d1a1db30c1c31f8a6faa8497cca82391888c5..45fa7856ce987bedc75058122b71091183d80e9b 100644 (file)
@@ -1,11 +1,23 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
-import { getPluginTestPath, installPlugin, setAccessTokensToServers } from '../../../shared/extra-utils'
+import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
+import {
+  getMyUserInformation,
+  getPluginTestPath,
+  installPlugin,
+  logout,
+  setAccessTokensToServers,
+  uninstallPlugin,
+  updateMyUser,
+  userLogin
+} from '../../../shared/extra-utils'
+import { User, UserRole } from '@shared/models'
+import { expect } from 'chai'
 
 describe('Test id and pass auth plugins', function () {
   let server: ServerInfo
+  let crashToken: string
 
   before(async function () {
     this.timeout(30000)
@@ -13,54 +25,97 @@ describe('Test id and pass auth plugins', function () {
     server = await flushAndRunServer(1)
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-id-pass-auth-one')
-    })
-
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-id-pass-auth-two')
-    })
+    for (const suffix of [ 'one', 'two', 'three' ]) {
+      await installPlugin({
+        url: server.url,
+        accessToken: server.accessToken,
+        path: getPluginTestPath('-id-pass-auth-' + suffix)
+      })
+    }
   })
 
-  it('Should not login', async function() {
-
+  it('Should not login', async function () {
+    await userLogin(server, { username: 'toto', password: 'password' }, 400)
   })
 
-  it('Should login Spyro, create the user and use the token', async function() {
+  it('Should login Spyro, create the user and use the token', async function () {
+    const accessToken = await userLogin(server, { username: 'spyro', password: 'spyro password' })
 
+    const res = await getMyUserInformation(server.url, accessToken)
+
+    const body: User = res.body
+    expect(body.username).to.equal('spyro')
+    expect(body.account.displayName).to.equal('Spyro the Dragon')
+    expect(body.role).to.equal(UserRole.USER)
   })
 
-  it('Should login Crash, create the user and use the token', async function() {
+  it('Should login Crash, create the user and use the token', async function () {
+    crashToken = await userLogin(server, { username: 'crash', password: 'crash password' })
+
+    const res = await getMyUserInformation(server.url, crashToken)
 
+    const body: User = res.body
+    expect(body.username).to.equal('crash')
+    expect(body.account.displayName).to.equal('Crash Bandicoot')
+    expect(body.role).to.equal(UserRole.MODERATOR)
   })
 
-  it('Should login the first Laguna, create the user and use the token', async function() {
+  it('Should login the first Laguna, create the user and use the token', async function () {
+    const accessToken = await userLogin(server, { username: 'laguna', password: 'laguna password' })
 
+    const res = await getMyUserInformation(server.url, accessToken)
+
+    const body: User = res.body
+    expect(body.username).to.equal('laguna')
+    expect(body.account.displayName).to.equal('laguna')
+    expect(body.role).to.equal(UserRole.USER)
   })
 
   it('Should update Crash profile', async function () {
+    await updateMyUser({
+      url: server.url,
+      accessToken: crashToken,
+      displayName: 'Beautiful Crash',
+      description: 'Mutant eastern barred bandicoot'
+    })
 
+    const res = await getMyUserInformation(server.url, crashToken)
+
+    const body: User = res.body
+    expect(body.account.displayName).to.equal('Beautiful Crash')
+    expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
   })
 
   it('Should logout Crash', async function () {
-
-    // test token
+    await logout(server.url, crashToken)
   })
 
-  it('Should have logged the Crash logout', async function () {
+  it('Should have logged out Crash', async function () {
+    await getMyUserInformation(server.url, crashToken, 401)
 
+    await waitUntilLog(server, 'On logout for auth 1 - 2')
   })
 
   it('Should login Crash and keep the old existing profile', async function () {
+    crashToken = await userLogin(server, { username: 'crash', password: 'crash password' })
 
+    const res = await getMyUserInformation(server.url, crashToken)
+
+    const body: User = res.body
+    expect(body.username).to.equal('crash')
+    expect(body.account.displayName).to.equal('Beautiful Crash')
+    expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
+    expect(body.role).to.equal(UserRole.MODERATOR)
   })
 
   it('Should uninstall the plugin one and do not login existing Crash', async function () {
+    await uninstallPlugin({
+      url: server.url,
+      accessToken: server.accessToken,
+      npmName: 'peertube-plugin-test-id-pass-auth-one'
+    })
 
+    await userLogin(server, { username: 'crash', password: 'crash password' }, 400)
   })
 
   after(async function () {
index ebccf7f7da933e5787e581b4b34c8dc693a113fa..2d12a486a6a895c08ad749343ac7df8c661b3f08 100644 (file)
@@ -37,6 +37,7 @@ declare module 'express' {
       bypassLogin?: {
         bypass: boolean
         pluginName: string
+        authName?: string
         user: {
           username: string
           email: string
@@ -45,6 +46,8 @@ declare module 'express' {
         }
       }
 
+      explicitLogout: boolean
+
       videoAll?: MVideoFullLight
       onlyImmutableVideo?: MVideoImmutable
       onlyVideo?: MVideoThumbnail
index 0c0993c1417e87dca3bb02bf8b92acadae5ed1af..bcabf2fec42bce2db8e0bc799d614d53a79fc7d4 100644 (file)
@@ -9,7 +9,11 @@ import { Logger } from 'winston'
 import { Router } from 'express'
 import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
 import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
-import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult } from '@shared/models/plugins/register-server-auth.model'
+import {
+  RegisterServerAuthExternalOptions,
+  RegisterServerAuthExternalResult,
+  RegisterServerAuthPassOptions
+} from '@shared/models/plugins/register-server-auth.model'
 
 export type PeerTubeHelpers = {
   logger: Logger
index 34ebbe71223e45bc98e5e86b98158c4f93bd90ce..dc46dcbc804727a45bbf307b4008232d2502a8fb 100644 (file)
@@ -3,10 +3,12 @@ import { UserRole } from '@shared/models'
 export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
 
 export interface RegisterServerAuthPassOptions {
-  type: 'id-and-pass'
+  // Authentication name (a plugin can register multiple auth strategies)
+  authName: string
 
   onLogout?: Function
 
+  // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
   getWeight(): number
 
   // Used by PeerTube to login a user
@@ -23,7 +25,8 @@ export interface RegisterServerAuthPassOptions {
 }
 
 export interface RegisterServerAuthExternalOptions {
-  type: 'external'
+  // Authentication name (a plugin can register multiple auth strategies)
+  authName: string
 
   onLogout?: Function
 }