diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/users/token.ts | 4 | ||||
-rw-r--r-- | server/initializers/constants.ts | 5 | ||||
-rw-r--r-- | server/lib/auth.ts | 13 | ||||
-rw-r--r-- | server/lib/oauth-model.ts | 2 | ||||
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 13 | ||||
-rw-r--r-- | server/lib/plugins/register-helpers-store.ts | 22 | ||||
-rw-r--r-- | server/tests/api/check-params/plugins.ts | 13 | ||||
-rw-r--r-- | server/tests/external-plugins/auth-ldap.ts | 14 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js | 67 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json | 20 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js | 31 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json | 20 | ||||
-rw-r--r-- | server/tests/plugins/external-auth.ts | 295 | ||||
-rw-r--r-- | server/tests/plugins/id-and-pass-auth.ts | 30 | ||||
-rw-r--r-- | server/tests/plugins/index.ts | 1 |
15 files changed, 527 insertions, 23 deletions
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index f4be228f6..41aa26769 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { handleIdAndPassLogin, handleTokenRevocation } from '@server/lib/auth' | 1 | import { handleLogin, handleTokenRevocation } from '@server/lib/auth' |
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import * as express from 'express' | 4 | import * as express from 'express' |
@@ -14,7 +14,7 @@ const loginRateLimiter = RateLimit({ | |||
14 | 14 | ||
15 | tokensRouter.post('/token', | 15 | tokensRouter.post('/token', |
16 | loginRateLimiter, | 16 | loginRateLimiter, |
17 | handleIdAndPassLogin, | 17 | handleLogin, |
18 | tokenSuccess | 18 | tokenSuccess |
19 | ) | 19 | ) |
20 | 20 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index c8623a5d4..c8e50dd53 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -645,6 +645,8 @@ const P2P_MEDIA_LOADER_PEER_VERSION = 2 | |||
645 | const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css' | 645 | const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css' |
646 | const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME) | 646 | const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME) |
647 | 647 | ||
648 | let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes | ||
649 | |||
648 | const DEFAULT_THEME_NAME = 'default' | 650 | const DEFAULT_THEME_NAME = 'default' |
649 | const DEFAULT_USER_THEME_NAME = 'instance-default' | 651 | const DEFAULT_USER_THEME_NAME = 'instance-default' |
650 | 652 | ||
@@ -686,6 +688,8 @@ if (isTestInstance() === true) { | |||
686 | FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 | 688 | FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 |
687 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000 | 689 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000 |
688 | OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2 | 690 | OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2 |
691 | |||
692 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 | ||
689 | } | 693 | } |
690 | 694 | ||
691 | updateWebserverUrls() | 695 | updateWebserverUrls() |
@@ -778,6 +782,7 @@ export { | |||
778 | VIDEO_VIEW_LIFETIME, | 782 | VIDEO_VIEW_LIFETIME, |
779 | CONTACT_FORM_LIFETIME, | 783 | CONTACT_FORM_LIFETIME, |
780 | VIDEO_PLAYLIST_PRIVACIES, | 784 | VIDEO_PLAYLIST_PRIVACIES, |
785 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME, | ||
781 | ASSETS_PATH, | 786 | ASSETS_PATH, |
782 | loadLanguages, | 787 | loadLanguages, |
783 | buildLanguages | 788 | buildLanguages |
diff --git a/server/lib/auth.ts b/server/lib/auth.ts index eaae5fdf3..2ef77bc9c 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' |
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { generateRandomString } from '@server/helpers/utils' | 3 | import { generateRandomString } from '@server/helpers/utils' |
4 | import { OAUTH_LIFETIME, WEBSERVER } from '@server/initializers/constants' | 4 | import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
5 | import { revokeToken } from '@server/lib/oauth-model' | 5 | import { revokeToken } from '@server/lib/oauth-model' |
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
@@ -35,7 +35,7 @@ const authBypassTokens = new Map<string, { | |||
35 | npmName: string | 35 | npmName: string |
36 | }>() | 36 | }>() |
37 | 37 | ||
38 | async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | 38 | async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) { |
39 | const grantType = req.body.grant_type | 39 | const grantType = req.body.grant_type |
40 | 40 | ||
41 | if (grantType === 'password') { | 41 | if (grantType === 'password') { |
@@ -90,10 +90,9 @@ async function onExternalUserAuthenticated (options: { | |||
90 | logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) | 90 | logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) |
91 | 91 | ||
92 | const bypassToken = await generateRandomString(32) | 92 | const bypassToken = await generateRandomString(32) |
93 | const tokenLifetime = 1000 * 60 * 5 // 5 minutes | ||
94 | 93 | ||
95 | const expires = new Date() | 94 | const expires = new Date() |
96 | expires.setTime(expires.getTime() + tokenLifetime) | 95 | expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME) |
97 | 96 | ||
98 | const user = buildUserResult(authResult) | 97 | const user = buildUserResult(authResult) |
99 | authBypassTokens.set(bypassToken, { | 98 | authBypassTokens.set(bypassToken, { |
@@ -108,7 +107,7 @@ async function onExternalUserAuthenticated (options: { | |||
108 | 107 | ||
109 | // --------------------------------------------------------------------------- | 108 | // --------------------------------------------------------------------------- |
110 | 109 | ||
111 | export { oAuthServer, handleIdAndPassLogin, onExternalUserAuthenticated, handleTokenRevocation } | 110 | export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation } |
112 | 111 | ||
113 | // --------------------------------------------------------------------------- | 112 | // --------------------------------------------------------------------------- |
114 | 113 | ||
@@ -212,7 +211,7 @@ function proxifyExternalAuthBypass (req: express.Request, res: express.Response) | |||
212 | 211 | ||
213 | const now = new Date() | 212 | const now = new Date() |
214 | if (now.getTime() > expires.getTime()) { | 213 | if (now.getTime() > expires.getTime()) { |
215 | logger.error('Cannot authenticate user with an expired bypass token') | 214 | logger.error('Cannot authenticate user with an expired external auth token') |
216 | return res.sendStatus(400) | 215 | return res.sendStatus(400) |
217 | } | 216 | } |
218 | 217 | ||
@@ -267,7 +266,7 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
267 | return { | 266 | return { |
268 | username: pluginResult.username, | 267 | username: pluginResult.username, |
269 | email: pluginResult.email, | 268 | email: pluginResult.email, |
270 | role: pluginResult.role || UserRole.USER, | 269 | role: pluginResult.role ?? UserRole.USER, |
271 | displayName: pluginResult.displayName || pluginResult.username | 270 | displayName: pluginResult.displayName || pluginResult.username |
272 | } | 271 | } |
273 | } | 272 | } |
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 8b9975bb4..8d8a6d85e 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts | |||
@@ -139,7 +139,7 @@ async function revokeToken (tokenInfo: { refreshToken: string }) { | |||
139 | 139 | ||
140 | if (token) { | 140 | if (token) { |
141 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { | 141 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { |
142 | PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName) | 142 | PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User) |
143 | } | 143 | } |
144 | 144 | ||
145 | clearCacheByToken(token.accessToken) | 145 | clearCacheByToken(token.accessToken) |
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index c64ca60aa..38336bcc6 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -21,7 +21,8 @@ import { ClientHtml } from '../client-html' | |||
21 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' | 21 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' |
22 | import { RegisterHelpersStore } from './register-helpers-store' | 22 | import { RegisterHelpersStore } from './register-helpers-store' |
23 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' | 23 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' |
24 | import { MOAuthTokenUser } from '@server/typings/models' | 24 | import { MOAuthTokenUser, MUser } from '@server/typings/models' |
25 | import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model' | ||
25 | 26 | ||
26 | export interface RegisteredPlugin { | 27 | export interface RegisteredPlugin { |
27 | npmName: string | 28 | npmName: string |
@@ -133,14 +134,14 @@ export class PluginManager implements ServerHook { | |||
133 | return this.translations[locale] || {} | 134 | return this.translations[locale] || {} |
134 | } | 135 | } |
135 | 136 | ||
136 | onLogout (npmName: string, authName: string) { | 137 | onLogout (npmName: string, authName: string, user: MUser) { |
137 | const auth = this.getAuth(npmName, authName) | 138 | const auth = this.getAuth(npmName, authName) |
138 | 139 | ||
139 | if (auth?.onLogout) { | 140 | if (auth?.onLogout) { |
140 | logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName) | 141 | logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName) |
141 | 142 | ||
142 | try { | 143 | try { |
143 | auth.onLogout() | 144 | auth.onLogout(user) |
144 | } catch (err) { | 145 | } catch (err) { |
145 | logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err }) | 146 | logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err }) |
146 | } | 147 | } |
@@ -478,8 +479,10 @@ export class PluginManager implements ServerHook { | |||
478 | const plugin = this.getRegisteredPluginOrTheme(npmName) | 479 | const plugin = this.getRegisteredPluginOrTheme(npmName) |
479 | if (!plugin || plugin.type !== PluginType.PLUGIN) return null | 480 | if (!plugin || plugin.type !== PluginType.PLUGIN) return null |
480 | 481 | ||
481 | return plugin.registerHelpersStore.getIdAndPassAuths() | 482 | let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths() |
482 | .find(a => a.authName === authName) | 483 | auths = auths.concat(plugin.registerHelpersStore.getExternalAuths()) |
484 | |||
485 | return auths.find(a => a.authName === authName) | ||
483 | } | 486 | } |
484 | 487 | ||
485 | // ###################### Private getters ###################### | 488 | // ###################### Private getters ###################### |
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts index 277f2b687..151196bf1 100644 --- a/server/lib/plugins/register-helpers-store.ts +++ b/server/lib/plugins/register-helpers-store.ts | |||
@@ -1,5 +1,12 @@ | |||
1 | import * as express from 'express' | ||
1 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
2 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PRIVACIES } from '@server/initializers/constants' | 3 | import { |
4 | VIDEO_CATEGORIES, | ||
5 | VIDEO_LANGUAGES, | ||
6 | VIDEO_LICENCES, | ||
7 | VIDEO_PLAYLIST_PRIVACIES, | ||
8 | VIDEO_PRIVACIES | ||
9 | } from '@server/initializers/constants' | ||
3 | import { onExternalUserAuthenticated } from '@server/lib/auth' | 10 | import { onExternalUserAuthenticated } from '@server/lib/auth' |
4 | import { PluginModel } from '@server/models/server/plugin' | 11 | import { PluginModel } from '@server/models/server/plugin' |
5 | import { RegisterServerOptions } from '@server/typings/plugins' | 12 | import { RegisterServerOptions } from '@server/typings/plugins' |
@@ -10,11 +17,15 @@ import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video- | |||
10 | import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model' | 17 | import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model' |
11 | import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model' | 18 | import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model' |
12 | import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' | 19 | import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' |
13 | import { RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult, RegisterServerAuthPassOptions, RegisterServerExternalAuthenticatedResult } from '@shared/models/plugins/register-server-auth.model' | 20 | import { |
21 | RegisterServerAuthExternalOptions, | ||
22 | RegisterServerAuthExternalResult, | ||
23 | RegisterServerAuthPassOptions, | ||
24 | RegisterServerExternalAuthenticatedResult | ||
25 | } from '@shared/models/plugins/register-server-auth.model' | ||
14 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' | 26 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' |
15 | import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' | 27 | import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' |
16 | import { serverHookObject } from '@shared/models/plugins/server-hook.model' | 28 | import { serverHookObject } from '@shared/models/plugins/server-hook.model' |
17 | import * as express from 'express' | ||
18 | import { buildPluginHelpers } from './plugin-helpers' | 29 | import { buildPluginHelpers } from './plugin-helpers' |
19 | 30 | ||
20 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' | 31 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' |
@@ -174,6 +185,11 @@ export class RegisterHelpersStore { | |||
174 | const self = this | 185 | const self = this |
175 | 186 | ||
176 | return (options: RegisterServerAuthExternalOptions) => { | 187 | return (options: RegisterServerAuthExternalOptions) => { |
188 | if (!options.authName || !options.onAuthRequest || typeof options.onAuthRequest !== 'function') { | ||
189 | logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName) | ||
190 | return | ||
191 | } | ||
192 | |||
177 | this.externalAuths.push(options) | 193 | this.externalAuths.push(options) |
178 | 194 | ||
179 | return { | 195 | return { |
diff --git a/server/tests/api/check-params/plugins.ts b/server/tests/api/check-params/plugins.ts index cf80b35c2..07ded26ee 100644 --- a/server/tests/api/check-params/plugins.ts +++ b/server/tests/api/check-params/plugins.ts | |||
@@ -64,6 +64,7 @@ describe('Test server plugins API validators', function () { | |||
64 | describe('With static plugin routes', function () { | 64 | describe('With static plugin routes', function () { |
65 | it('Should fail with an unknown plugin name/plugin version', async function () { | 65 | it('Should fail with an unknown plugin name/plugin version', async function () { |
66 | const paths = [ | 66 | const paths = [ |
67 | '/plugins/' + pluginName + '/0.0.1/auth/fake-auth', | ||
67 | '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png', | 68 | '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png', |
68 | '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js', | 69 | '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js', |
69 | '/themes/' + themeName + '/0.0.1/static/images/chocobo.png', | 70 | '/themes/' + themeName + '/0.0.1/static/images/chocobo.png', |
@@ -86,6 +87,7 @@ describe('Test server plugins API validators', function () { | |||
86 | 87 | ||
87 | it('Should fail with invalid versions', async function () { | 88 | it('Should fail with invalid versions', async function () { |
88 | const paths = [ | 89 | const paths = [ |
90 | '/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth', | ||
89 | '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png', | 91 | '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png', |
90 | '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js', | 92 | '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js', |
91 | '/themes/' + themeName + '/1/static/images/chocobo.png', | 93 | '/themes/' + themeName + '/1/static/images/chocobo.png', |
@@ -112,6 +114,12 @@ describe('Test server plugins API validators', function () { | |||
112 | } | 114 | } |
113 | }) | 115 | }) |
114 | 116 | ||
117 | it('Should fail with an unknown auth name', async function () { | ||
118 | const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth' | ||
119 | |||
120 | await makeGetRequest({ url: server.url, path, statusCodeExpected: 404 }) | ||
121 | }) | ||
122 | |||
115 | it('Should fail with an unknown static file', async function () { | 123 | it('Should fail with an unknown static file', async function () { |
116 | const paths = [ | 124 | const paths = [ |
117 | '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png', | 125 | '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png', |
@@ -145,6 +153,9 @@ describe('Test server plugins API validators', function () { | |||
145 | for (const p of paths) { | 153 | for (const p of paths) { |
146 | await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 }) | 154 | await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 }) |
147 | } | 155 | } |
156 | |||
157 | const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth' | ||
158 | await makeGetRequest({ url: server.url, path: authPath, statusCodeExpected: 302 }) | ||
148 | }) | 159 | }) |
149 | }) | 160 | }) |
150 | 161 | ||
@@ -462,6 +473,8 @@ describe('Test server plugins API validators', function () { | |||
462 | }) | 473 | }) |
463 | 474 | ||
464 | it('Should succeed with the correct parameters', async function () { | 475 | it('Should succeed with the correct parameters', async function () { |
476 | this.timeout(10000) | ||
477 | |||
465 | const it = [ | 478 | const it = [ |
466 | { suffix: 'install', status: 200 }, | 479 | { suffix: 'install', status: 200 }, |
467 | { suffix: 'update', status: 200 }, | 480 | { suffix: 'update', status: 200 }, |
diff --git a/server/tests/external-plugins/auth-ldap.ts b/server/tests/external-plugins/auth-ldap.ts index 7aee986c7..0f0a08532 100644 --- a/server/tests/external-plugins/auth-ldap.ts +++ b/server/tests/external-plugins/auth-ldap.ts | |||
@@ -1,10 +1,18 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { getMyUserInformation, installPlugin, setAccessTokensToServers, updatePluginSettings, userLogin, uploadVideo, uninstallPlugin } from '../../../shared/extra-utils' | ||
5 | import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers' | ||
6 | import { User } from '@shared/models/users/user.model' | ||
7 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { User } from '@shared/models/users/user.model' | ||
6 | import { | ||
7 | getMyUserInformation, | ||
8 | installPlugin, | ||
9 | setAccessTokensToServers, | ||
10 | uninstallPlugin, | ||
11 | updatePluginSettings, | ||
12 | uploadVideo, | ||
13 | userLogin | ||
14 | } from '../../../shared/extra-utils' | ||
15 | import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers' | ||
8 | 16 | ||
9 | describe('Official plugin auth-ldap', function () { | 17 | describe('Official plugin auth-ldap', function () { |
10 | let server: ServerInfo | 18 | let server: ServerInfo |
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js new file mode 100644 index 000000000..f29fd1f30 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js | |||
@@ -0,0 +1,67 @@ | |||
1 | async function register ({ | ||
2 | registerExternalAuth, | ||
3 | peertubeHelpers | ||
4 | }) { | ||
5 | { | ||
6 | const result = registerExternalAuth({ | ||
7 | authName: 'external-auth-1', | ||
8 | authDisplayName: 'External Auth 1', | ||
9 | onLogout: user => peertubeHelpers.logger.info('On logout %s', user.username), | ||
10 | onAuthRequest: (req, res) => { | ||
11 | const username = req.query.username | ||
12 | |||
13 | result.userAuthenticated({ | ||
14 | req, | ||
15 | res, | ||
16 | username, | ||
17 | email: username + '@example.com' | ||
18 | }) | ||
19 | } | ||
20 | }) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | const result = registerExternalAuth({ | ||
25 | authName: 'external-auth-2', | ||
26 | authDisplayName: 'External Auth 2', | ||
27 | onAuthRequest: (req, res) => { | ||
28 | result.userAuthenticated({ | ||
29 | req, | ||
30 | res, | ||
31 | username: 'kefka', | ||
32 | email: 'kefka@example.com', | ||
33 | role: 0, | ||
34 | displayName: 'Kefka Palazzo' | ||
35 | }) | ||
36 | }, | ||
37 | hookTokenValidity: (options) => { | ||
38 | if (options.type === 'refresh') { | ||
39 | return { valid: false } | ||
40 | } | ||
41 | |||
42 | if (options.type === 'access') { | ||
43 | const token = options.token | ||
44 | const now = new Date() | ||
45 | now.setTime(now.getTime() - 5000) | ||
46 | |||
47 | const createdAt = new Date(token.createdAt) | ||
48 | |||
49 | return { valid: createdAt.getTime() >= now.getTime() } | ||
50 | } | ||
51 | |||
52 | return { valid: true } | ||
53 | } | ||
54 | }) | ||
55 | } | ||
56 | } | ||
57 | |||
58 | async function unregister () { | ||
59 | return | ||
60 | } | ||
61 | |||
62 | module.exports = { | ||
63 | register, | ||
64 | unregister | ||
65 | } | ||
66 | |||
67 | // ########################################################################### | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json b/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json new file mode 100644 index 000000000..22814b047 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-external-auth-one", | ||
3 | "version": "0.0.1", | ||
4 | "description": "External auth one", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js new file mode 100644 index 000000000..34fec1bb3 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js | |||
@@ -0,0 +1,31 @@ | |||
1 | async function register ({ | ||
2 | registerExternalAuth, | ||
3 | peertubeHelpers | ||
4 | }) { | ||
5 | { | ||
6 | const result = registerExternalAuth({ | ||
7 | authName: 'external-auth-3', | ||
8 | authDisplayName: 'External Auth 3', | ||
9 | onAuthRequest: (req, res) => { | ||
10 | result.userAuthenticated({ | ||
11 | req, | ||
12 | res, | ||
13 | username: 'cid', | ||
14 | email: 'cid@example.com', | ||
15 | displayName: 'Cid Marquez' | ||
16 | }) | ||
17 | } | ||
18 | }) | ||
19 | } | ||
20 | } | ||
21 | |||
22 | async function unregister () { | ||
23 | return | ||
24 | } | ||
25 | |||
26 | module.exports = { | ||
27 | register, | ||
28 | unregister | ||
29 | } | ||
30 | |||
31 | // ########################################################################### | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json b/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json new file mode 100644 index 000000000..a5ca4d07a --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-external-auth-two", | ||
3 | "version": "0.0.1", | ||
4 | "description": "External auth two", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts new file mode 100644 index 000000000..a72b2829b --- /dev/null +++ b/server/tests/plugins/external-auth.ts | |||
@@ -0,0 +1,295 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { expect } from 'chai' | ||
5 | import { ServerConfig, User, UserRole } from '@shared/models' | ||
6 | import { | ||
7 | decodeQueryString, | ||
8 | getConfig, | ||
9 | getExternalAuth, | ||
10 | getMyUserInformation, | ||
11 | getPluginTestPath, | ||
12 | installPlugin, | ||
13 | loginUsingExternalToken, | ||
14 | logout, | ||
15 | refreshToken, | ||
16 | setAccessTokensToServers, | ||
17 | uninstallPlugin, | ||
18 | updateMyUser, | ||
19 | wait | ||
20 | } from '../../../shared/extra-utils' | ||
21 | import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' | ||
22 | |||
23 | async function loginExternal (options: { | ||
24 | server: ServerInfo | ||
25 | npmName: string | ||
26 | authName: string | ||
27 | username: string | ||
28 | query?: any | ||
29 | statusCodeExpected?: number | ||
30 | }) { | ||
31 | const res = await getExternalAuth({ | ||
32 | url: options.server.url, | ||
33 | npmName: options.npmName, | ||
34 | npmVersion: '0.0.1', | ||
35 | authName: options.authName, | ||
36 | query: options.query, | ||
37 | statusCodeExpected: options.statusCodeExpected || 302 | ||
38 | }) | ||
39 | |||
40 | if (res.status !== 302) return | ||
41 | |||
42 | const location = res.header.location | ||
43 | const { externalAuthToken } = decodeQueryString(location) | ||
44 | |||
45 | const resLogin = await loginUsingExternalToken( | ||
46 | options.server, | ||
47 | options.username, | ||
48 | externalAuthToken as string | ||
49 | ) | ||
50 | |||
51 | return resLogin.body | ||
52 | } | ||
53 | |||
54 | describe('Test external auth plugins', function () { | ||
55 | let server: ServerInfo | ||
56 | |||
57 | let cyanAccessToken: string | ||
58 | let cyanRefreshToken: string | ||
59 | |||
60 | let kefkaAccessToken: string | ||
61 | let kefkaRefreshToken: string | ||
62 | |||
63 | let externalAuthToken: string | ||
64 | |||
65 | before(async function () { | ||
66 | this.timeout(30000) | ||
67 | |||
68 | server = await flushAndRunServer(1) | ||
69 | await setAccessTokensToServers([ server ]) | ||
70 | |||
71 | for (const suffix of [ 'one', 'two' ]) { | ||
72 | await installPlugin({ | ||
73 | url: server.url, | ||
74 | accessToken: server.accessToken, | ||
75 | path: getPluginTestPath('-external-auth-' + suffix) | ||
76 | }) | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | it('Should display the correct configuration', async function () { | ||
81 | const res = await getConfig(server.url) | ||
82 | |||
83 | const config: ServerConfig = res.body | ||
84 | |||
85 | const auths = config.plugin.registeredExternalAuths | ||
86 | expect(auths).to.have.lengthOf(3) | ||
87 | |||
88 | const auth2 = auths.find((a) => a.authName === 'external-auth-2') | ||
89 | expect(auth2).to.exist | ||
90 | expect(auth2.authDisplayName).to.equal('External Auth 2') | ||
91 | expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one') | ||
92 | }) | ||
93 | |||
94 | it('Should redirect for a Cyan login', async function () { | ||
95 | const res = await getExternalAuth({ | ||
96 | url: server.url, | ||
97 | npmName: 'test-external-auth-one', | ||
98 | npmVersion: '0.0.1', | ||
99 | authName: 'external-auth-1', | ||
100 | query: { | ||
101 | username: 'cyan' | ||
102 | }, | ||
103 | statusCodeExpected: 302 | ||
104 | }) | ||
105 | |||
106 | const location = res.header.location | ||
107 | expect(location.startsWith('/login?')).to.be.true | ||
108 | |||
109 | const searchParams = decodeQueryString(location) | ||
110 | |||
111 | expect(searchParams.externalAuthToken).to.exist | ||
112 | expect(searchParams.username).to.equal('cyan') | ||
113 | |||
114 | externalAuthToken = searchParams.externalAuthToken as string | ||
115 | }) | ||
116 | |||
117 | it('Should reject auto external login with a missing or invalid token', async function () { | ||
118 | await loginUsingExternalToken(server, 'cyan', '', 400) | ||
119 | await loginUsingExternalToken(server, 'cyan', 'blabla', 400) | ||
120 | }) | ||
121 | |||
122 | it('Should reject auto external login with a missing or invalid username', async function () { | ||
123 | await loginUsingExternalToken(server, '', externalAuthToken, 400) | ||
124 | await loginUsingExternalToken(server, '', externalAuthToken, 400) | ||
125 | }) | ||
126 | |||
127 | it('Should reject auto external login with an expired token', async function () { | ||
128 | this.timeout(15000) | ||
129 | |||
130 | await wait(5000) | ||
131 | |||
132 | await loginUsingExternalToken(server, 'cyan', externalAuthToken, 400) | ||
133 | |||
134 | await waitUntilLog(server, 'expired external auth token') | ||
135 | }) | ||
136 | |||
137 | it('Should auto login Cyan, create the user and use the token', async function () { | ||
138 | { | ||
139 | const res = await loginExternal({ | ||
140 | server, | ||
141 | npmName: 'test-external-auth-one', | ||
142 | authName: 'external-auth-1', | ||
143 | query: { | ||
144 | username: 'cyan' | ||
145 | }, | ||
146 | username: 'cyan' | ||
147 | }) | ||
148 | |||
149 | cyanAccessToken = res.access_token | ||
150 | cyanRefreshToken = res.refresh_token | ||
151 | } | ||
152 | |||
153 | { | ||
154 | const res = await getMyUserInformation(server.url, cyanAccessToken) | ||
155 | |||
156 | const body: User = res.body | ||
157 | expect(body.username).to.equal('cyan') | ||
158 | expect(body.account.displayName).to.equal('cyan') | ||
159 | expect(body.email).to.equal('cyan@example.com') | ||
160 | expect(body.role).to.equal(UserRole.USER) | ||
161 | } | ||
162 | }) | ||
163 | |||
164 | it('Should auto login Kefka, create the user and use the token', async function () { | ||
165 | { | ||
166 | const res = await loginExternal({ | ||
167 | server, | ||
168 | npmName: 'test-external-auth-one', | ||
169 | authName: 'external-auth-2', | ||
170 | username: 'kefka' | ||
171 | }) | ||
172 | |||
173 | kefkaAccessToken = res.access_token | ||
174 | kefkaRefreshToken = res.refresh_token | ||
175 | } | ||
176 | |||
177 | { | ||
178 | const res = await getMyUserInformation(server.url, kefkaAccessToken) | ||
179 | |||
180 | const body: User = res.body | ||
181 | expect(body.username).to.equal('kefka') | ||
182 | expect(body.account.displayName).to.equal('Kefka Palazzo') | ||
183 | expect(body.email).to.equal('kefka@example.com') | ||
184 | expect(body.role).to.equal(UserRole.ADMINISTRATOR) | ||
185 | } | ||
186 | }) | ||
187 | |||
188 | it('Should refresh Cyan token, but not Kefka token', async function () { | ||
189 | { | ||
190 | const resRefresh = await refreshToken(server, cyanRefreshToken) | ||
191 | cyanAccessToken = resRefresh.body.access_token | ||
192 | cyanRefreshToken = resRefresh.body.refresh_token | ||
193 | |||
194 | const res = await getMyUserInformation(server.url, cyanAccessToken) | ||
195 | const user: User = res.body | ||
196 | expect(user.username).to.equal('cyan') | ||
197 | } | ||
198 | |||
199 | { | ||
200 | await refreshToken(server, kefkaRefreshToken, 400) | ||
201 | } | ||
202 | }) | ||
203 | |||
204 | it('Should update Cyan profile', async function () { | ||
205 | await updateMyUser({ | ||
206 | url: server.url, | ||
207 | accessToken: cyanAccessToken, | ||
208 | displayName: 'Cyan Garamonde', | ||
209 | description: 'Retainer to the king of Doma' | ||
210 | }) | ||
211 | |||
212 | const res = await getMyUserInformation(server.url, cyanAccessToken) | ||
213 | |||
214 | const body: User = res.body | ||
215 | expect(body.account.displayName).to.equal('Cyan Garamonde') | ||
216 | expect(body.account.description).to.equal('Retainer to the king of Doma') | ||
217 | }) | ||
218 | |||
219 | it('Should logout Cyan', async function () { | ||
220 | await logout(server.url, cyanAccessToken) | ||
221 | }) | ||
222 | |||
223 | it('Should have logged out Cyan', async function () { | ||
224 | await waitUntilLog(server, 'On logout cyan') | ||
225 | |||
226 | await getMyUserInformation(server.url, cyanAccessToken, 401) | ||
227 | }) | ||
228 | |||
229 | it('Should login Cyan and keep the old existing profile', async function () { | ||
230 | { | ||
231 | const res = await loginExternal({ | ||
232 | server, | ||
233 | npmName: 'test-external-auth-one', | ||
234 | authName: 'external-auth-1', | ||
235 | query: { | ||
236 | username: 'cyan' | ||
237 | }, | ||
238 | username: 'cyan' | ||
239 | }) | ||
240 | |||
241 | cyanAccessToken = res.access_token | ||
242 | } | ||
243 | |||
244 | const res = await getMyUserInformation(server.url, cyanAccessToken) | ||
245 | |||
246 | const body: User = res.body | ||
247 | expect(body.username).to.equal('cyan') | ||
248 | expect(body.account.displayName).to.equal('Cyan Garamonde') | ||
249 | expect(body.account.description).to.equal('Retainer to the king of Doma') | ||
250 | expect(body.role).to.equal(UserRole.USER) | ||
251 | }) | ||
252 | |||
253 | it('Should reject token of Kefka by the plugin hook', async function () { | ||
254 | this.timeout(10000) | ||
255 | |||
256 | await wait(5000) | ||
257 | |||
258 | await getMyUserInformation(server.url, kefkaAccessToken, 401) | ||
259 | }) | ||
260 | |||
261 | it('Should uninstall the plugin one and do not login Cyan', async function () { | ||
262 | await uninstallPlugin({ | ||
263 | url: server.url, | ||
264 | accessToken: server.accessToken, | ||
265 | npmName: 'peertube-plugin-test-external-auth-one' | ||
266 | }) | ||
267 | |||
268 | await loginExternal({ | ||
269 | server, | ||
270 | npmName: 'test-external-auth-one', | ||
271 | authName: 'external-auth-1', | ||
272 | query: { | ||
273 | username: 'cyan' | ||
274 | }, | ||
275 | username: 'cyan', | ||
276 | statusCodeExpected: 404 | ||
277 | }) | ||
278 | }) | ||
279 | |||
280 | it('Should display the correct configuration', async function () { | ||
281 | const res = await getConfig(server.url) | ||
282 | |||
283 | const config: ServerConfig = res.body | ||
284 | |||
285 | const auths = config.plugin.registeredExternalAuths | ||
286 | expect(auths).to.have.lengthOf(1) | ||
287 | |||
288 | const auth2 = auths.find((a) => a.authName === 'external-auth-2') | ||
289 | expect(auth2).to.not.exist | ||
290 | }) | ||
291 | |||
292 | after(async function () { | ||
293 | await cleanupTests([ server ]) | ||
294 | }) | ||
295 | }) | ||
diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts index c6382435d..6c10730aa 100644 --- a/server/tests/plugins/id-and-pass-auth.ts +++ b/server/tests/plugins/id-and-pass-auth.ts | |||
@@ -12,9 +12,9 @@ import { | |||
12 | updateMyUser, | 12 | updateMyUser, |
13 | userLogin, | 13 | userLogin, |
14 | wait, | 14 | wait, |
15 | login, refreshToken | 15 | login, refreshToken, getConfig |
16 | } from '../../../shared/extra-utils' | 16 | } from '../../../shared/extra-utils' |
17 | import { User, UserRole } from '@shared/models' | 17 | import { User, UserRole, ServerConfig } from '@shared/models' |
18 | import { expect } from 'chai' | 18 | import { expect } from 'chai' |
19 | 19 | ||
20 | describe('Test id and pass auth plugins', function () { | 20 | describe('Test id and pass auth plugins', function () { |
@@ -41,6 +41,20 @@ describe('Test id and pass auth plugins', function () { | |||
41 | } | 41 | } |
42 | }) | 42 | }) |
43 | 43 | ||
44 | it('Should display the correct configuration', async function () { | ||
45 | const res = await getConfig(server.url) | ||
46 | |||
47 | const config: ServerConfig = res.body | ||
48 | |||
49 | const auths = config.plugin.registeredIdAndPassAuths | ||
50 | expect(auths).to.have.lengthOf(8) | ||
51 | |||
52 | const crashAuth = auths.find(a => a.authName === 'crash-auth') | ||
53 | expect(crashAuth).to.exist | ||
54 | expect(crashAuth.npmName).to.equal('peertube-plugin-test-id-pass-auth-one') | ||
55 | expect(crashAuth.weight).to.equal(50) | ||
56 | }) | ||
57 | |||
44 | it('Should not login', async function () { | 58 | it('Should not login', async function () { |
45 | await userLogin(server, { username: 'toto', password: 'password' }, 400) | 59 | await userLogin(server, { username: 'toto', password: 'password' }, 400) |
46 | }) | 60 | }) |
@@ -175,6 +189,18 @@ describe('Test id and pass auth plugins', function () { | |||
175 | await userLogin(server, { username: 'crash', password: 'crash password' }, 400) | 189 | await userLogin(server, { username: 'crash', password: 'crash password' }, 400) |
176 | }) | 190 | }) |
177 | 191 | ||
192 | it('Should display the correct configuration', async function () { | ||
193 | const res = await getConfig(server.url) | ||
194 | |||
195 | const config: ServerConfig = res.body | ||
196 | |||
197 | const auths = config.plugin.registeredIdAndPassAuths | ||
198 | expect(auths).to.have.lengthOf(6) | ||
199 | |||
200 | const crashAuth = auths.find(a => a.authName === 'crash-auth') | ||
201 | expect(crashAuth).to.not.exist | ||
202 | }) | ||
203 | |||
178 | after(async function () { | 204 | after(async function () { |
179 | await cleanupTests([ server ]) | 205 | await cleanupTests([ server ]) |
180 | }) | 206 | }) |
diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts index 8aa30654a..d2bd69131 100644 --- a/server/tests/plugins/index.ts +++ b/server/tests/plugins/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import './action-hooks' | 1 | import './action-hooks' |
2 | import './id-and-pass-auth' | 2 | import './id-and-pass-auth' |
3 | import './external-auth' | ||
3 | import './filter-hooks' | 4 | import './filter-hooks' |
4 | import './translations' | 5 | import './translations' |
5 | import './video-constants' | 6 | import './video-constants' |