@@ -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)) | |||
@@ -1,5 +1,5 @@ | |||
import * as express from 'express' | |||
import { getFormattedObjects} from '../../helpers/utils' | |||
import { getFormattedObjects } from '../../helpers/utils' | |||
import { | |||
asyncMiddleware, | |||
authenticate, | |||
@@ -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 { | |||
@@ -1,6 +1,6 @@ | |||
import * as express from 'express' | |||
import 'multer' | |||
import { getFormattedObjects} from '../../../helpers/utils' | |||
import { getFormattedObjects } from '../../../helpers/utils' | |||
import { | |||
asyncMiddleware, | |||
asyncRetryTransactionMiddleware, | |||
@@ -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()) | |||
@@ -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 }) | |||
} |
@@ -1,5 +1,5 @@ | |||
import * as express from 'express' | |||
import { getFormattedObjects} from '../../helpers/utils' | |||
import { getFormattedObjects } from '../../helpers/utils' | |||
import { | |||
asyncMiddleware, | |||
asyncRetryTransactionMiddleware, | |||
@@ -1,5 +1,5 @@ | |||
import * as express from 'express' | |||
import { getFormattedObjects} from '../../helpers/utils' | |||
import { getFormattedObjects } from '../../helpers/utils' | |||
import { | |||
asyncMiddleware, | |||
asyncRetryTransactionMiddleware, | |||
@@ -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, | |||
@@ -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) { | |||
@@ -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 | |||
} | |||
@@ -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> { | |||
@@ -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) | |||
} | |||
} | |||
@@ -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 } : {} | |||
@@ -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) | |||
@@ -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 | |||
@@ -97,6 +97,9 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> { | |||
@Column | |||
refreshTokenExpiresAt: Date | |||
@Column | |||
authName: string | |||
@CreatedAt | |||
createdAt: Date | |||
@@ -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 () { | |||
@@ -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' | |||
}) | |||
} | |||
@@ -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') | |||
@@ -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') | |||
@@ -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 () { | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
} | |||