aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-08-03 10:10:26 +0200
committerChocobozzz <me@florianbigard.com>2022-08-03 10:10:26 +0200
commit0b6f531653a7a24f82ad65564479a70a9326301a (patch)
treef65d9c80e0e8ced86a8a9f7b00952bb04413a5b7
parent35a0a924830d84f9ec28c129ec85cb1f45011fb8 (diff)
downloadPeerTube-0b6f531653a7a24f82ad65564479a70a9326301a.tar.gz
PeerTube-0b6f531653a7a24f82ad65564479a70a9326301a.tar.zst
PeerTube-0b6f531653a7a24f82ad65564479a70a9326301a.zip
Suffix external auth username on conflict
-rw-r--r--server/helpers/custom-validators/user-notifications.ts2
-rw-r--r--server/lib/auth/oauth-model.ts9
-rw-r--r--server/lib/local-actor.ts18
-rw-r--r--server/lib/user.ts17
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js16
-rw-r--r--server/tests/plugins/external-auth.ts39
6 files changed, 81 insertions, 20 deletions
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts
index 252c107db..2de13ca09 100644
--- a/server/helpers/custom-validators/user-notifications.ts
+++ b/server/helpers/custom-validators/user-notifications.ts
@@ -1,5 +1,5 @@
1import validator from 'validator' 1import validator from 'validator'
2import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 2import { UserNotificationSettingValue } from '@shared/models'
3import { exists } from './misc' 3import { exists } from './misc'
4 4
5function isUserNotificationTypeValid (value: any) { 5function isUserNotificationTypeValid (value: any) {
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts
index d9cf32827..322b69e3a 100644
--- a/server/lib/auth/oauth-model.ts
+++ b/server/lib/auth/oauth-model.ts
@@ -1,7 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { AccessDeniedError } from '@node-oauth/oauth2-server' 2import { AccessDeniedError } from '@node-oauth/oauth2-server'
3import { PluginManager } from '@server/lib/plugins/plugin-manager' 3import { PluginManager } from '@server/lib/plugins/plugin-manager'
4import { ActorModel } from '@server/models/actor/actor'
5import { MOAuthClient } from '@server/types/models' 4import { MOAuthClient } from '@server/types/models'
6import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 5import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
7import { MUser } from '@server/types/models/user/user' 6import { MUser } from '@server/types/models/user/user'
@@ -12,6 +11,7 @@ import { CONFIG } from '../../initializers/config'
12import { OAuthClientModel } from '../../models/oauth/oauth-client' 11import { OAuthClientModel } from '../../models/oauth/oauth-client'
13import { OAuthTokenModel } from '../../models/oauth/oauth-token' 12import { OAuthTokenModel } from '../../models/oauth/oauth-token'
14import { UserModel } from '../../models/user/user' 13import { UserModel } from '../../models/user/user'
14import { findAvailableLocalActorName } from '../local-actor'
15import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' 15import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user'
16import { TokensCache } from './tokens-cache' 16import { TokensCache } from './tokens-cache'
17 17
@@ -225,13 +225,12 @@ async function createUserFromExternal (pluginAuth: string, options: {
225 role: UserRole 225 role: UserRole
226 displayName: string 226 displayName: string
227}) { 227}) {
228 // Check an actor does not already exists with that name (removed user) 228 const username = await findAvailableLocalActorName(options.username)
229 const actor = await ActorModel.loadLocalByName(options.username)
230 if (actor) return null
231 229
232 const userToCreate = buildUser({ 230 const userToCreate = buildUser({
233 ...pick(options, [ 'username', 'email', 'role' ]), 231 ...pick(options, [ 'email', 'role' ]),
234 232
233 username,
235 emailVerified: null, 234 emailVerified: null,
236 password: null, 235 password: null,
237 pluginAuth 236 pluginAuth
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts
index 1d9be76e2..8c10ed700 100644
--- a/server/lib/local-actor.ts
+++ b/server/lib/local-actor.ts
@@ -1,6 +1,7 @@
1import { remove } from 'fs-extra' 1import { remove } from 'fs-extra'
2import LRUCache from 'lru-cache' 2import LRUCache from 'lru-cache'
3import { join } from 'path' 3import { join } from 'path'
4import { Transaction } from 'sequelize/types'
4import { ActorModel } from '@server/models/actor/actor' 5import { ActorModel } from '@server/models/actor/actor'
5import { getLowercaseExtension } from '@shared/core-utils' 6import { getLowercaseExtension } from '@shared/core-utils'
6import { buildUUID } from '@shared/extra-utils' 7import { buildUUID } from '@shared/extra-utils'
@@ -87,6 +88,22 @@ async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MC
87 88
88// --------------------------------------------------------------------------- 89// ---------------------------------------------------------------------------
89 90
91async function findAvailableLocalActorName (baseActorName: string, transaction?: Transaction) {
92 let actor = await ActorModel.loadLocalByName(baseActorName, transaction)
93 if (!actor) return baseActorName
94
95 for (let i = 1; i < 30; i++) {
96 const name = `${baseActorName}-${i}`
97
98 actor = await ActorModel.loadLocalByName(name, transaction)
99 if (!actor) return name
100 }
101
102 throw new Error('Cannot find available actor local name (too much iterations).')
103}
104
105// ---------------------------------------------------------------------------
106
90function downloadActorImageFromWorker (options: { 107function downloadActorImageFromWorker (options: {
91 fileUrl: string 108 fileUrl: string
92 filename: string 109 filename: string
@@ -109,6 +126,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.
109export { 126export {
110 actorImagePathUnsafeCache, 127 actorImagePathUnsafeCache,
111 updateLocalActorImageFiles, 128 updateLocalActorImageFiles,
129 findAvailableLocalActorName,
112 downloadActorImageFromWorker, 130 downloadActorImageFromWorker,
113 deleteLocalActorImageFile, 131 deleteLocalActorImageFile,
114 downloadImageFromWorker, 132 downloadImageFromWorker,
diff --git a/server/lib/user.ts b/server/lib/user.ts
index f4ffae0e4..2e433da04 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -3,13 +3,11 @@ import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
4import { UserModel } from '@server/models/user/user' 4import { UserModel } from '@server/models/user/user'
5import { MActorDefault } from '@server/types/models/actor' 5import { MActorDefault } from '@server/types/models/actor'
6import { buildUUID } from '@shared/extra-utils'
7import { ActivityPubActorType } from '../../shared/models/activitypub' 6import { ActivityPubActorType } from '../../shared/models/activitypub'
8import { UserAdminFlag, UserNotificationSetting, UserNotificationSettingValue, UserRole } from '../../shared/models/users' 7import { UserAdminFlag, UserNotificationSetting, UserNotificationSettingValue, UserRole } from '../../shared/models/users'
9import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' 8import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
10import { sequelizeTypescript } from '../initializers/database' 9import { sequelizeTypescript } from '../initializers/database'
11import { AccountModel } from '../models/account/account' 10import { AccountModel } from '../models/account/account'
12import { ActorModel } from '../models/actor/actor'
13import { UserNotificationSettingModel } from '../models/user/user-notification-setting' 11import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
14import { MAccountDefault, MChannelActor } from '../types/models' 12import { MAccountDefault, MChannelActor } from '../types/models'
15import { MUser, MUserDefault, MUserId } from '../types/models/user' 13import { MUser, MUserDefault, MUserId } from '../types/models/user'
@@ -17,7 +15,7 @@ import { generateAndSaveActorKeys } from './activitypub/actors'
17import { getLocalAccountActivityPubUrl } from './activitypub/url' 15import { getLocalAccountActivityPubUrl } from './activitypub/url'
18import { Emailer } from './emailer' 16import { Emailer } from './emailer'
19import { LiveQuotaStore } from './live/live-quota-store' 17import { LiveQuotaStore } from './live/live-quota-store'
20import { buildActorInstance } from './local-actor' 18import { buildActorInstance, findAvailableLocalActorName } from './local-actor'
21import { Redis } from './redis' 19import { Redis } from './redis'
22import { createLocalVideoChannel } from './video-channel' 20import { createLocalVideoChannel } from './video-channel'
23import { createWatchLaterPlaylist } from './video-playlist' 21import { createWatchLaterPlaylist } from './video-playlist'
@@ -71,6 +69,8 @@ function buildUser (options: {
71 }) 69 })
72} 70}
73 71
72// ---------------------------------------------------------------------------
73
74async function createUserAccountAndChannelAndPlaylist (parameters: { 74async function createUserAccountAndChannelAndPlaylist (parameters: {
75 userToCreate: MUser 75 userToCreate: MUser
76 userDisplayName?: string 76 userDisplayName?: string
@@ -157,6 +157,8 @@ async function createApplicationActor (applicationId: number) {
157 return accountCreated 157 return accountCreated
158} 158}
159 159
160// ---------------------------------------------------------------------------
161
160async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { 162async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
161 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) 163 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
162 let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString 164 let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
@@ -169,6 +171,8 @@ async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
169 Emailer.Instance.addVerifyEmailJob(username, email, url) 171 Emailer.Instance.addVerifyEmailJob(username, email, url)
170} 172}
171 173
174// ---------------------------------------------------------------------------
175
172async function getOriginalVideoFileTotalFromUser (user: MUserId) { 176async function getOriginalVideoFileTotalFromUser (user: MUserId) {
173 // Don't use sequelize because we need to use a sub query 177 // Don't use sequelize because we need to use a sub query
174 const query = UserModel.generateUserQuotaBaseSQL({ 178 const query = UserModel.generateUserQuotaBaseSQL({
@@ -263,12 +267,7 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
263async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) { 267async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) {
264 if (channelNames) return channelNames 268 if (channelNames) return channelNames
265 269
266 let channelName = user.username + '_channel' 270 const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction)
267
268 // Conflict, generate uuid instead
269 const actor = await ActorModel.loadLocalByName(channelName, transaction)
270 if (actor) channelName = buildUUID()
271
272 const videoChannelDisplayName = `Main ${user.username} channel` 271 const videoChannelDisplayName = `Main ${user.username} channel`
273 272
274 return { 273 return {
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
index 1604a7c41..755dbb62b 100644
--- a/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js
@@ -65,6 +65,22 @@ async function register ({
65 } 65 }
66 }) 66 })
67 } 67 }
68
69 {
70 const result = registerExternalAuth({
71 authName: 'external-auth-7',
72 authDisplayName: () => 'External Auth 7',
73 onAuthRequest: (req, res) => {
74 result.userAuthenticated({
75 req,
76 res,
77 username: 'existing_user2',
78 email: 'custom_email_existing_user2@example.com',
79 displayName: 'Existing user 2'
80 })
81 }
82 })
83 }
68} 84}
69 85
70async function unregister () { 86async function unregister () {
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
index 583100671..042681dbe 100644
--- a/server/tests/plugins/external-auth.ts
+++ b/server/tests/plugins/external-auth.ts
@@ -58,7 +58,14 @@ describe('Test external auth plugins', function () {
58 before(async function () { 58 before(async function () {
59 this.timeout(30000) 59 this.timeout(30000)
60 60
61 server = await createSingleServer(1) 61 server = await createSingleServer(1, {
62 rates_limit: {
63 login: {
64 max: 30
65 }
66 }
67 })
68
62 await setAccessTokensToServers([ server ]) 69 await setAccessTokensToServers([ server ])
63 70
64 for (const suffix of [ 'one', 'two', 'three' ]) { 71 for (const suffix of [ 'one', 'two', 'three' ]) {
@@ -70,7 +77,7 @@ describe('Test external auth plugins', function () {
70 const config = await server.config.getConfig() 77 const config = await server.config.getConfig()
71 78
72 const auths = config.plugin.registeredExternalAuths 79 const auths = config.plugin.registeredExternalAuths
73 expect(auths).to.have.lengthOf(8) 80 expect(auths).to.have.lengthOf(9)
74 81
75 const auth2 = auths.find((a) => a.authName === 'external-auth-2') 82 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
76 expect(auth2).to.exist 83 expect(auth2).to.exist
@@ -275,7 +282,7 @@ describe('Test external auth plugins', function () {
275 const config = await server.config.getConfig() 282 const config = await server.config.getConfig()
276 283
277 const auths = config.plugin.registeredExternalAuths 284 const auths = config.plugin.registeredExternalAuths
278 expect(auths).to.have.lengthOf(7) 285 expect(auths).to.have.lengthOf(8)
279 286
280 const auth1 = auths.find(a => a.authName === 'external-auth-2') 287 const auth1 = auths.find(a => a.authName === 'external-auth-2')
281 expect(auth1).to.not.exist 288 expect(auth1).to.not.exist
@@ -318,7 +325,7 @@ describe('Test external auth plugins', function () {
318 }) 325 })
319 }) 326 })
320 327
321 it('Should not login an existing user', async function () { 328 it('Should not login an existing user email', async function () {
322 await server.users.create({ username: 'existing_user', password: 'super_password' }) 329 await server.users.create({ username: 'existing_user', password: 'super_password' })
323 330
324 await loginExternal({ 331 await loginExternal({
@@ -330,11 +337,33 @@ describe('Test external auth plugins', function () {
330 }) 337 })
331 }) 338 })
332 339
340 it('Should be able to login an existing user username and channel', async function () {
341 await server.users.create({ username: 'existing_user2' })
342 await server.users.create({ username: 'existing_user2-1_channel' })
343
344 // Test twice to ensure we don't generate a username on every login
345 for (let i = 0; i < 2; i++) {
346 const res = await loginExternal({
347 server,
348 npmName: 'test-external-auth-two',
349 authName: 'external-auth-7',
350 username: 'existing_user2'
351 })
352
353 const token = res.access_token
354
355 const myInfo = await server.users.getMyInfo({ token })
356 expect(myInfo.username).to.equal('existing_user2-1')
357
358 expect(myInfo.videoChannels[0].name).to.equal('existing_user2-1_channel-1')
359 }
360 })
361
333 it('Should display the correct configuration', async function () { 362 it('Should display the correct configuration', async function () {
334 const config = await server.config.getConfig() 363 const config = await server.config.getConfig()
335 364
336 const auths = config.plugin.registeredExternalAuths 365 const auths = config.plugin.registeredExternalAuths
337 expect(auths).to.have.lengthOf(6) 366 expect(auths).to.have.lengthOf(7)
338 367
339 const auth2 = auths.find((a) => a.authName === 'external-auth-2') 368 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
340 expect(auth2).to.not.exist 369 expect(auth2).to.not.exist