diff options
author | Chocobozzz <me@florianbigard.com> | 2022-08-03 10:10:26 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-08-03 10:10:26 +0200 |
commit | 0b6f531653a7a24f82ad65564479a70a9326301a (patch) | |
tree | f65d9c80e0e8ced86a8a9f7b00952bb04413a5b7 | |
parent | 35a0a924830d84f9ec28c129ec85cb1f45011fb8 (diff) | |
download | PeerTube-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.ts | 2 | ||||
-rw-r--r-- | server/lib/auth/oauth-model.ts | 9 | ||||
-rw-r--r-- | server/lib/local-actor.ts | 18 | ||||
-rw-r--r-- | server/lib/user.ts | 17 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js | 16 | ||||
-rw-r--r-- | server/tests/plugins/external-auth.ts | 39 |
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 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 2 | import { UserNotificationSettingValue } from '@shared/models' |
3 | import { exists } from './misc' | 3 | import { exists } from './misc' |
4 | 4 | ||
5 | function isUserNotificationTypeValid (value: any) { | 5 | function 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' | 2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' |
3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
4 | import { ActorModel } from '@server/models/actor/actor' | ||
5 | import { MOAuthClient } from '@server/types/models' | 4 | import { MOAuthClient } from '@server/types/models' |
6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 5 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
7 | import { MUser } from '@server/types/models/user/user' | 6 | import { MUser } from '@server/types/models/user/user' |
@@ -12,6 +11,7 @@ import { CONFIG } from '../../initializers/config' | |||
12 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 11 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
13 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' | 12 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
14 | import { UserModel } from '../../models/user/user' | 13 | import { UserModel } from '../../models/user/user' |
14 | import { findAvailableLocalActorName } from '../local-actor' | ||
15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' | 15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' |
16 | import { TokensCache } from './tokens-cache' | 16 | import { 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 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import LRUCache from 'lru-cache' | 2 | import LRUCache from 'lru-cache' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { Transaction } from 'sequelize/types' | ||
4 | import { ActorModel } from '@server/models/actor/actor' | 5 | import { ActorModel } from '@server/models/actor/actor' |
5 | import { getLowercaseExtension } from '@shared/core-utils' | 6 | import { getLowercaseExtension } from '@shared/core-utils' |
6 | import { buildUUID } from '@shared/extra-utils' | 7 | import { buildUUID } from '@shared/extra-utils' |
@@ -87,6 +88,22 @@ async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MC | |||
87 | 88 | ||
88 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
89 | 90 | ||
91 | async 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 | |||
90 | function downloadActorImageFromWorker (options: { | 107 | function 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. | |||
109 | export { | 126 | export { |
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' | |||
3 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import { UserModel } from '@server/models/user/user' | 4 | import { UserModel } from '@server/models/user/user' |
5 | import { MActorDefault } from '@server/types/models/actor' | 5 | import { MActorDefault } from '@server/types/models/actor' |
6 | import { buildUUID } from '@shared/extra-utils' | ||
7 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 6 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
8 | import { UserAdminFlag, UserNotificationSetting, UserNotificationSettingValue, UserRole } from '../../shared/models/users' | 7 | import { UserAdminFlag, UserNotificationSetting, UserNotificationSettingValue, UserRole } from '../../shared/models/users' |
9 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' | 8 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' |
10 | import { sequelizeTypescript } from '../initializers/database' | 9 | import { sequelizeTypescript } from '../initializers/database' |
11 | import { AccountModel } from '../models/account/account' | 10 | import { AccountModel } from '../models/account/account' |
12 | import { ActorModel } from '../models/actor/actor' | ||
13 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' | 11 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' |
14 | import { MAccountDefault, MChannelActor } from '../types/models' | 12 | import { MAccountDefault, MChannelActor } from '../types/models' |
15 | import { MUser, MUserDefault, MUserId } from '../types/models/user' | 13 | import { MUser, MUserDefault, MUserId } from '../types/models/user' |
@@ -17,7 +15,7 @@ import { generateAndSaveActorKeys } from './activitypub/actors' | |||
17 | import { getLocalAccountActivityPubUrl } from './activitypub/url' | 15 | import { getLocalAccountActivityPubUrl } from './activitypub/url' |
18 | import { Emailer } from './emailer' | 16 | import { Emailer } from './emailer' |
19 | import { LiveQuotaStore } from './live/live-quota-store' | 17 | import { LiveQuotaStore } from './live/live-quota-store' |
20 | import { buildActorInstance } from './local-actor' | 18 | import { buildActorInstance, findAvailableLocalActorName } from './local-actor' |
21 | import { Redis } from './redis' | 19 | import { Redis } from './redis' |
22 | import { createLocalVideoChannel } from './video-channel' | 20 | import { createLocalVideoChannel } from './video-channel' |
23 | import { createWatchLaterPlaylist } from './video-playlist' | 21 | import { createWatchLaterPlaylist } from './video-playlist' |
@@ -71,6 +69,8 @@ function buildUser (options: { | |||
71 | }) | 69 | }) |
72 | } | 70 | } |
73 | 71 | ||
72 | // --------------------------------------------------------------------------- | ||
73 | |||
74 | async function createUserAccountAndChannelAndPlaylist (parameters: { | 74 | async 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 | |||
160 | async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { | 162 | async 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 | |||
172 | async function getOriginalVideoFileTotalFromUser (user: MUserId) { | 176 | async 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 | | |||
263 | async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) { | 267 | async 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 | ||
70 | async function unregister () { | 86 | async 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 |