aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/users/user-edit/user-create.component.ts4
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts20
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts3
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts19
-rw-r--r--client/src/app/core/auth/auth-user.model.ts9
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts3
-rw-r--r--server/controllers/api/users/index.ts7
-rw-r--r--server/middlewares/validators/users.ts72
-rw-r--r--server/tests/api/check-params/users.ts166
10 files changed, 227 insertions, 78 deletions
diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts
index 9a6801806..3b57a49c6 100644
--- a/client/src/app/+admin/users/user-edit/user-create.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-create.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { Notifier, ServerService } from '@app/core' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { UserCreate, UserRole } from '../../../../../../shared' 4import { UserCreate, UserRole } from '../../../../../../shared'
5import { UserEdit } from './user-edit' 5import { UserEdit } from './user-edit'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -8,7 +8,6 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' 8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
9import { ConfigService } from '@app/+admin/config/shared/config.service' 9import { ConfigService } from '@app/+admin/config/shared/config.service'
10import { UserService } from '@app/shared' 10import { UserService } from '@app/shared'
11import { UserAdminFlag } from '@shared/models/users/user-flag.model'
12 11
13@Component({ 12@Component({
14 selector: 'my-user-create', 13 selector: 'my-user-create',
@@ -22,6 +21,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
22 protected serverService: ServerService, 21 protected serverService: ServerService,
23 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
24 protected configService: ConfigService, 23 protected configService: ConfigService,
24 protected auth: AuthService,
25 private userValidatorsService: UserValidatorsService, 25 private userValidatorsService: UserValidatorsService,
26 private router: Router, 26 private router: Router,
27 private notifier: Notifier, 27 private notifier: Notifier,
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 400bac5d4..cb0f36f05 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -41,7 +41,7 @@
41 <label i18n for="role">Role</label> 41 <label i18n for="role">Role</label>
42 <div class="peertube-select-container"> 42 <div class="peertube-select-container">
43 <select id="role" formControlName="role"> 43 <select id="role" formControlName="role">
44 <option *ngFor="let role of roles" [value]="role.value"> 44 <option *ngFor="let role of getRoles()" [value]="role.value">
45 {{ role.label }} 45 {{ role.label }}
46 </option> 46 </option>
47 </select> 47 </select>
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index ee6d2c489..6625d65d6 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -1,22 +1,34 @@
1import { ServerService } from '../../../core' 1import { AuthService, ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' 3import { USER_ROLE_LABELS, UserRole, VideoResolution } from '../../../../../../shared'
4import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
5import { UserAdminFlag } from '@shared/models/users/user-flag.model' 5import { UserAdminFlag } from '@shared/models/users/user-flag.model'
6 6
7export abstract class UserEdit extends FormReactive { 7export abstract class UserEdit extends FormReactive {
8 videoQuotaOptions: { value: string, label: string }[] = [] 8 videoQuotaOptions: { value: string, label: string }[] = []
9 videoQuotaDailyOptions: { value: string, label: string }[] = [] 9 videoQuotaDailyOptions: { value: string, label: string }[] = []
10 roles = Object.keys(USER_ROLE_LABELS)
11 .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
12 username: string 10 username: string
13 userId: number 11 userId: number
14 12
15 protected abstract serverService: ServerService 13 protected abstract serverService: ServerService
16 protected abstract configService: ConfigService 14 protected abstract configService: ConfigService
15 protected abstract auth: AuthService
17 abstract isCreation (): boolean 16 abstract isCreation (): boolean
18 abstract getFormButtonTitle (): string 17 abstract getFormButtonTitle (): string
19 18
19 getRoles () {
20 const authUser = this.auth.getUser()
21
22 if (authUser.role === UserRole.ADMINISTRATOR) {
23 return Object.keys(USER_ROLE_LABELS)
24 .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
25 }
26
27 return [
28 { value: UserRole.USER.toString(), label: USER_ROLE_LABELS[UserRole.USER] }
29 ]
30 }
31
20 isTranscodingInformationDisplayed () { 32 isTranscodingInformationDisplayed () {
21 const formVideoQuota = parseInt(this.form.value['videoQuota'], 10) 33 const formVideoQuota = parseInt(this.form.value['videoQuota'], 10)
22 34
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts
index 04b2935f4..c7052a925 100644
--- a/client/src/app/+admin/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-update.component.ts
@@ -1,7 +1,7 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs' 3import { Subscription } from 'rxjs'
4import { Notifier } from '@app/core' 4import { AuthService, Notifier } from '@app/core'
5import { ServerService } from '../../../core' 5import { ServerService } from '../../../core'
6import { UserEdit } from './user-edit' 6import { UserEdit } from './user-edit'
7import { User, UserUpdate } from '../../../../../../shared' 7import { User, UserUpdate } from '../../../../../../shared'
@@ -29,6 +29,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
29 protected formValidatorService: FormValidatorService, 29 protected formValidatorService: FormValidatorService,
30 protected serverService: ServerService, 30 protected serverService: ServerService,
31 protected configService: ConfigService, 31 protected configService: ConfigService,
32 protected auth: AuthService,
32 private userValidatorsService: UserValidatorsService, 33 private userValidatorsService: UserValidatorsService,
33 private route: ActivatedRoute, 34 private route: ActivatedRoute,
34 private router: Router, 35 private router: Router,
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index c9c790689..ab82713b2 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { AuthService, Notifier } from '@app/core'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { ConfirmService, ServerService } from '../../../core' 4import { ConfirmService, ServerService } from '../../../core'
5import { RestPagination, RestTable, UserService } from '../../../shared' 5import { RestPagination, RestTable, UserService } from '../../../shared'
@@ -30,11 +30,16 @@ export class UserListComponent extends RestTable implements OnInit {
30 private confirmService: ConfirmService, 30 private confirmService: ConfirmService,
31 private serverService: ServerService, 31 private serverService: ServerService,
32 private userService: UserService, 32 private userService: UserService,
33 private auth: AuthService,
33 private i18n: I18n 34 private i18n: I18n
34 ) { 35 ) {
35 super() 36 super()
36 } 37 }
37 38
39 get authUser () {
40 return this.auth.getUser()
41 }
42
38 get requiresEmailVerification () { 43 get requiresEmailVerification () {
39 return this.serverService.getConfig().signup.requiresEmailVerification 44 return this.serverService.getConfig().signup.requiresEmailVerification
40 } 45 }
@@ -45,22 +50,26 @@ export class UserListComponent extends RestTable implements OnInit {
45 this.bulkUserActions = [ 50 this.bulkUserActions = [
46 { 51 {
47 label: this.i18n('Delete'), 52 label: this.i18n('Delete'),
48 handler: users => this.removeUsers(users) 53 handler: users => this.removeUsers(users),
54 isDisplayed: users => users.every(u => this.authUser.canManage(u))
49 }, 55 },
50 { 56 {
51 label: this.i18n('Ban'), 57 label: this.i18n('Ban'),
52 handler: users => this.openBanUserModal(users), 58 handler: users => this.openBanUserModal(users),
53 isDisplayed: users => users.every(u => u.blocked === false) 59 isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === false)
54 }, 60 },
55 { 61 {
56 label: this.i18n('Unban'), 62 label: this.i18n('Unban'),
57 handler: users => this.unbanUsers(users), 63 handler: users => this.unbanUsers(users),
58 isDisplayed: users => users.every(u => u.blocked === true) 64 isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === true)
59 }, 65 },
60 { 66 {
61 label: this.i18n('Set Email as Verified'), 67 label: this.i18n('Set Email as Verified'),
62 handler: users => this.setEmailsAsVerified(users), 68 handler: users => this.setEmailsAsVerified(users),
63 isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false) 69 isDisplayed: users => {
70 return this.requiresEmailVerification &&
71 users.every(u => this.authUser.canManage(u) && !u.blocked && u.emailVerified === false)
72 }
64 } 73 }
65 ] 74 ]
66 } 75 }
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts
index abb11fdc2..334ede0cd 100644
--- a/client/src/app/core/auth/auth-user.model.ts
+++ b/client/src/app/core/auth/auth-user.model.ts
@@ -139,6 +139,15 @@ export class AuthUser extends User {
139 return hasUserRight(this.role, right) 139 return hasUserRight(this.role, right)
140 } 140 }
141 141
142 canManage (user: ServerUserModel) {
143 const myRole = this.role
144
145 if (myRole === UserRole.ADMINISTRATOR) return true
146
147 // I'm a moderator: I can only manage users
148 return user.role === UserRole.USER
149 }
150
142 save () { 151 save () {
143 peertubeLocalStorage.setItem(AuthUser.KEYS.ID, this.id.toString()) 152 peertubeLocalStorage.setItem(AuthUser.KEYS.ID, this.id.toString())
144 peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username) 153 peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username)
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
index 24f717821..e9d4c1437 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -33,6 +33,7 @@ export class UserModerationDropdownComponent implements OnChanges {
33 private serverService: ServerService, 33 private serverService: ServerService,
34 private userService: UserService, 34 private userService: UserService,
35 private blocklistService: BlocklistService, 35 private blocklistService: BlocklistService,
36 private auth: AuthService,
36 private i18n: I18n 37 private i18n: I18n
37 ) { } 38 ) { }
38 39
@@ -230,7 +231,7 @@ export class UserModerationDropdownComponent implements OnChanges {
230 231
231 if (this.user && authUser.id === this.user.id) return 232 if (this.user && authUser.id === this.user.id) return
232 233
233 if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) { 234 if (this.user && authUser.hasRight(UserRight.MANAGE_USERS) && authUser.canManage(this.user)) {
234 this.userActions.push([ 235 this.userActions.push([
235 { 236 {
236 label: this.i18n('Edit'), 237 label: this.i18n('Edit'),
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 63747a0a9..ae40e86f8 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -31,7 +31,8 @@ import {
31 usersAskSendVerifyEmailValidator, 31 usersAskSendVerifyEmailValidator,
32 usersBlockingValidator, 32 usersBlockingValidator,
33 usersResetPasswordValidator, 33 usersResetPasswordValidator,
34 usersVerifyEmailValidator 34 usersVerifyEmailValidator,
35 ensureCanManageUser
35} from '../../../middlewares/validators' 36} from '../../../middlewares/validators'
36import { UserModel } from '../../../models/account/user' 37import { UserModel } from '../../../models/account/user'
37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 38import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -97,12 +98,14 @@ usersRouter.post('/:id/block',
97 authenticate, 98 authenticate,
98 ensureUserHasRight(UserRight.MANAGE_USERS), 99 ensureUserHasRight(UserRight.MANAGE_USERS),
99 asyncMiddleware(usersBlockingValidator), 100 asyncMiddleware(usersBlockingValidator),
101 ensureCanManageUser,
100 asyncMiddleware(blockUser) 102 asyncMiddleware(blockUser)
101) 103)
102usersRouter.post('/:id/unblock', 104usersRouter.post('/:id/unblock',
103 authenticate, 105 authenticate,
104 ensureUserHasRight(UserRight.MANAGE_USERS), 106 ensureUserHasRight(UserRight.MANAGE_USERS),
105 asyncMiddleware(usersBlockingValidator), 107 asyncMiddleware(usersBlockingValidator),
108 ensureCanManageUser,
106 asyncMiddleware(unblockUser) 109 asyncMiddleware(unblockUser)
107) 110)
108 111
@@ -132,6 +135,7 @@ usersRouter.put('/:id',
132 authenticate, 135 authenticate,
133 ensureUserHasRight(UserRight.MANAGE_USERS), 136 ensureUserHasRight(UserRight.MANAGE_USERS),
134 asyncMiddleware(usersUpdateValidator), 137 asyncMiddleware(usersUpdateValidator),
138 ensureCanManageUser,
135 asyncMiddleware(updateUser) 139 asyncMiddleware(updateUser)
136) 140)
137 141
@@ -139,6 +143,7 @@ usersRouter.delete('/:id',
139 authenticate, 143 authenticate,
140 ensureUserHasRight(UserRight.MANAGE_USERS), 144 ensureUserHasRight(UserRight.MANAGE_USERS),
141 asyncMiddleware(usersRemoveValidator), 145 asyncMiddleware(usersRemoveValidator),
146 ensureCanManageUser,
142 asyncMiddleware(removeUser) 147 asyncMiddleware(removeUser)
143) 148)
144 149
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index da92c715d..16d297047 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -30,6 +30,7 @@ import { UserRegister } from '../../../shared/models/users/user-register.model'
30import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 30import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
31import { isThemeRegistered } from '../../lib/plugins/theme-utils' 31import { isThemeRegistered } from '../../lib/plugins/theme-utils'
32import { doesVideoExist } from '../../helpers/middlewares' 32import { doesVideoExist } from '../../helpers/middlewares'
33import { UserRole } from '../../../shared/models/users'
33 34
34const usersAddValidator = [ 35const usersAddValidator = [
35 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), 36 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
@@ -46,6 +47,12 @@ const usersAddValidator = [
46 if (areValidationErrors(req, res)) return 47 if (areValidationErrors(req, res)) return
47 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return 48 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
48 49
50 const authUser = res.locals.oauth.token.User
51 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
52 return res.status(403)
53 .json({ error: 'You can only create users (and not administrators or moderators' })
54 }
55
49 return next() 56 return next()
50 } 57 }
51] 58]
@@ -75,21 +82,18 @@ const usersRegisterValidator = [
75 if (body.channel) { 82 if (body.channel) {
76 if (!body.channel.name || !body.channel.displayName) { 83 if (!body.channel.name || !body.channel.displayName) {
77 return res.status(400) 84 return res.status(400)
78 .send({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) 85 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
79 .end()
80 } 86 }
81 87
82 if (body.channel.name === body.username) { 88 if (body.channel.name === body.username) {
83 return res.status(400) 89 return res.status(400)
84 .send({ error: 'Channel name cannot be the same than user username.' }) 90 .json({ error: 'Channel name cannot be the same than user username.' })
85 .end()
86 } 91 }
87 92
88 const existing = await ActorModel.loadLocalByName(body.channel.name) 93 const existing = await ActorModel.loadLocalByName(body.channel.name)
89 if (existing) { 94 if (existing) {
90 return res.status(409) 95 return res.status(409)
91 .send({ error: `Channel with name ${body.channel.name} already exists.` }) 96 .json({ error: `Channel with name ${body.channel.name} already exists.` })
92 .end()
93 } 97 }
94 } 98 }
95 99
@@ -109,8 +113,7 @@ const usersRemoveValidator = [
109 const user = res.locals.user 113 const user = res.locals.user
110 if (user.username === 'root') { 114 if (user.username === 'root') {
111 return res.status(400) 115 return res.status(400)
112 .send({ error: 'Cannot remove the root user' }) 116 .json({ error: 'Cannot remove the root user' })
113 .end()
114 } 117 }
115 118
116 return next() 119 return next()
@@ -130,8 +133,7 @@ const usersBlockingValidator = [
130 const user = res.locals.user 133 const user = res.locals.user
131 if (user.username === 'root') { 134 if (user.username === 'root') {
132 return res.status(400) 135 return res.status(400)
133 .send({ error: 'Cannot block the root user' }) 136 .json({ error: 'Cannot block the root user' })
134 .end()
135 } 137 }
136 138
137 return next() 139 return next()
@@ -143,7 +145,7 @@ const deleteMeValidator = [
143 const user = res.locals.oauth.token.User 145 const user = res.locals.oauth.token.User
144 if (user.username === 'root') { 146 if (user.username === 'root') {
145 return res.status(400) 147 return res.status(400)
146 .send({ error: 'You cannot delete your root account.' }) 148 .json({ error: 'You cannot delete your root account.' })
147 .end() 149 .end()
148 } 150 }
149 151
@@ -170,8 +172,7 @@ const usersUpdateValidator = [
170 const user = res.locals.user 172 const user = res.locals.user
171 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { 173 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
172 return res.status(400) 174 return res.status(400)
173 .send({ error: 'Cannot change root role.' }) 175 .json({ error: 'Cannot change root role.' })
174 .end()
175 } 176 }
176 177
177 return next() 178 return next()
@@ -216,15 +217,14 @@ const usersUpdateMeValidator = [
216 if (req.body.password || req.body.email) { 217 if (req.body.password || req.body.email) {
217 if (!req.body.currentPassword) { 218 if (!req.body.currentPassword) {
218 return res.status(400) 219 return res.status(400)
219 .send({ error: 'currentPassword parameter is missing.' }) 220 .json({ error: 'currentPassword parameter is missing.' })
220 .end() 221 .end()
221 } 222 }
222 223
223 const user = res.locals.oauth.token.User 224 const user = res.locals.oauth.token.User
224 if (await user.isPasswordMatch(req.body.currentPassword) !== true) { 225 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
225 return res.status(401) 226 return res.status(401)
226 .send({ error: 'currentPassword is invalid.' }) 227 .json({ error: 'currentPassword is invalid.' })
227 .end()
228 } 228 }
229 } 229 }
230 230
@@ -265,8 +265,7 @@ const ensureUserRegistrationAllowed = [
265 const allowed = await isSignupAllowed() 265 const allowed = await isSignupAllowed()
266 if (allowed === false) { 266 if (allowed === false) {
267 return res.status(403) 267 return res.status(403)
268 .send({ error: 'User registration is not enabled or user limit is reached.' }) 268 .json({ error: 'User registration is not enabled or user limit is reached.' })
269 .end()
270 } 269 }
271 270
272 return next() 271 return next()
@@ -279,8 +278,7 @@ const ensureUserRegistrationAllowedForIP = [
279 278
280 if (allowed === false) { 279 if (allowed === false) {
281 return res.status(403) 280 return res.status(403)
282 .send({ error: 'You are not on a network authorized for registration.' }) 281 .json({ error: 'You are not on a network authorized for registration.' })
283 .end()
284 } 282 }
285 283
286 return next() 284 return next()
@@ -323,8 +321,7 @@ const usersResetPasswordValidator = [
323 if (redisVerificationString !== req.body.verificationString) { 321 if (redisVerificationString !== req.body.verificationString) {
324 return res 322 return res
325 .status(403) 323 .status(403)
326 .send({ error: 'Invalid verification string.' }) 324 .json({ error: 'Invalid verification string.' })
327 .end()
328 } 325 }
329 326
330 return next() 327 return next()
@@ -371,8 +368,7 @@ const usersVerifyEmailValidator = [
371 if (redisVerificationString !== req.body.verificationString) { 368 if (redisVerificationString !== req.body.verificationString) {
372 return res 369 return res
373 .status(403) 370 .status(403)
374 .send({ error: 'Invalid verification string.' }) 371 .json({ error: 'Invalid verification string.' })
375 .end()
376 } 372 }
377 373
378 return next() 374 return next()
@@ -389,14 +385,26 @@ const ensureAuthUserOwnsAccountValidator = [
389 385
390 if (res.locals.account.id !== user.Account.id) { 386 if (res.locals.account.id !== user.Account.id) {
391 return res.status(403) 387 return res.status(403)
392 .send({ error: 'Only owner can access ratings list.' }) 388 .json({ error: 'Only owner can access ratings list.' })
393 .end()
394 } 389 }
395 390
396 return next() 391 return next()
397 } 392 }
398] 393]
399 394
395const ensureCanManageUser = [
396 (req: express.Request, res: express.Response, next: express.NextFunction) => {
397 const authUser = res.locals.oauth.token.User
398 const onUser = res.locals.user
399
400 if (authUser.role === UserRole.ADMINISTRATOR) return next()
401 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
402
403 return res.status(403)
404 .json({ error: 'A moderator can only manager users.' })
405 }
406]
407
400// --------------------------------------------------------------------------- 408// ---------------------------------------------------------------------------
401 409
402export { 410export {
@@ -416,7 +424,8 @@ export {
416 usersAskSendVerifyEmailValidator, 424 usersAskSendVerifyEmailValidator,
417 usersVerifyEmailValidator, 425 usersVerifyEmailValidator,
418 userAutocompleteValidator, 426 userAutocompleteValidator,
419 ensureAuthUserOwnsAccountValidator 427 ensureAuthUserOwnsAccountValidator,
428 ensureCanManageUser
420} 429}
421 430
422// --------------------------------------------------------------------------- 431// ---------------------------------------------------------------------------
@@ -434,16 +443,14 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email:
434 443
435 if (user) { 444 if (user) {
436 res.status(409) 445 res.status(409)
437 .send({ error: 'User with this username or email already exists.' }) 446 .json({ error: 'User with this username or email already exists.' })
438 .end()
439 return false 447 return false
440 } 448 }
441 449
442 const actor = await ActorModel.loadLocalByName(username) 450 const actor = await ActorModel.loadLocalByName(username)
443 if (actor) { 451 if (actor) {
444 res.status(409) 452 res.status(409)
445 .send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) 453 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
446 .end()
447 return false 454 return false
448 } 455 }
449 456
@@ -456,8 +463,7 @@ async function checkUserExist (finder: () => Bluebird<UserModel>, res: express.R
456 if (!user) { 463 if (!user) {
457 if (abortResponse === true) { 464 if (abortResponse === true) {
458 res.status(404) 465 res.status(404)
459 .send({ error: 'User not found' }) 466 .json({ error: 'User not found' })
460 .end()
461 } 467 }
462 468
463 return false 469 return false
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 5b788e328..939b919ed 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -3,7 +3,7 @@
3import { omit } from 'lodash' 3import { omit } from 'lodash'
4import 'mocha' 4import 'mocha'
5import { join } from 'path' 5import { join } from 'path'
6import { UserRole, VideoImport, VideoImportState } from '../../../../shared' 6import { User, UserRole, VideoImport, VideoImportState } from '../../../../shared'
7 7
8import { 8import {
9 addVideoChannel, 9 addVideoChannel,
@@ -44,35 +44,79 @@ describe('Test users API validators', function () {
44 const path = '/api/v1/users/' 44 const path = '/api/v1/users/'
45 let userId: number 45 let userId: number
46 let rootId: number 46 let rootId: number
47 let moderatorId: number
47 let videoId: number 48 let videoId: number
48 let server: ServerInfo 49 let server: ServerInfo
49 let serverWithRegistrationDisabled: ServerInfo 50 let serverWithRegistrationDisabled: ServerInfo
50 let userAccessToken = '' 51 let userAccessToken = ''
52 let moderatorAccessToken = ''
51 let channelId: number 53 let channelId: number
52 const user = {
53 username: 'user1',
54 password: 'my super password'
55 }
56 54
57 // --------------------------------------------------------------- 55 // ---------------------------------------------------------------
58 56
59 before(async function () { 57 before(async function () {
60 this.timeout(30000) 58 this.timeout(30000)
61 59
62 server = await flushAndRunServer(1) 60 {
63 serverWithRegistrationDisabled = await flushAndRunServer(2) 61 const res = await Promise.all([
62 flushAndRunServer(1, { signup: { limit: 7 } }),
63 flushAndRunServer(2)
64 ])
64 65
65 await setAccessTokensToServers([ server ]) 66 server = res[0]
67 serverWithRegistrationDisabled = res[1]
66 68
67 const videoQuota = 42000000 69 await setAccessTokensToServers([ server ])
68 await createUser({ 70 }
69 url: server.url, 71
70 accessToken: server.accessToken, 72 {
71 username: user.username, 73 const user = {
72 password: user.password, 74 username: 'user1',
73 videoQuota: videoQuota 75 password: 'my super password'
74 }) 76 }
75 userAccessToken = await userLogin(server, user) 77
78 const videoQuota = 42000000
79 await createUser({
80 url: server.url,
81 accessToken: server.accessToken,
82 username: user.username,
83 password: user.password,
84 videoQuota: videoQuota
85 })
86 userAccessToken = await userLogin(server, user)
87 }
88
89 {
90 const moderator = {
91 username: 'moderator1',
92 password: 'super password'
93 }
94
95 await createUser({
96 url: server.url,
97 accessToken: server.accessToken,
98 username: moderator.username,
99 password: moderator.password,
100 role: UserRole.MODERATOR
101 })
102
103 moderatorAccessToken = await userLogin(server, moderator)
104 }
105
106 {
107 const moderator = {
108 username: 'moderator2',
109 password: 'super password'
110 }
111
112 await createUser({
113 url: server.url,
114 accessToken: server.accessToken,
115 username: moderator.username,
116 password: moderator.password,
117 role: UserRole.MODERATOR
118 })
119 }
76 120
77 { 121 {
78 const res = await getMyUserInformation(server.url, server.accessToken) 122 const res = await getMyUserInformation(server.url, server.accessToken)
@@ -83,6 +127,15 @@ describe('Test users API validators', function () {
83 const res = await uploadVideo(server.url, server.accessToken, {}) 127 const res = await uploadVideo(server.url, server.accessToken, {})
84 videoId = res.body.video.id 128 videoId = res.body.video.id
85 } 129 }
130
131 {
132 const res = await getUsersList(server.url, server.accessToken)
133 const users: User[] = res.body.data
134
135 userId = users.find(u => u.username === 'user1').id
136 rootId = users.find(u => u.username === 'root').id
137 moderatorId = users.find(u => u.username === 'moderator2').id
138 }
86 }) 139 })
87 140
88 describe('When listing users', function () { 141 describe('When listing users', function () {
@@ -251,6 +304,32 @@ describe('Test users API validators', function () {
251 }) 304 })
252 }) 305 })
253 306
307 it('Should fail to create a moderator or an admin with a moderator', async function () {
308 for (const role of [ UserRole.MODERATOR, UserRole.ADMINISTRATOR ]) {
309 const fields = immutableAssign(baseCorrectParams, { role })
310
311 await makePostBodyRequest({
312 url: server.url,
313 path,
314 token: moderatorAccessToken,
315 fields,
316 statusCodeExpected: 403
317 })
318 }
319 })
320
321 it('Should succeed to create a user with a moderator', async function () {
322 const fields = immutableAssign(baseCorrectParams, { username: 'a4656', email: 'a4656@example.com', role: UserRole.USER })
323
324 await makePostBodyRequest({
325 url: server.url,
326 path,
327 token: moderatorAccessToken,
328 fields,
329 statusCodeExpected: 200
330 })
331 })
332
254 it('Should succeed with the correct params', async function () { 333 it('Should succeed with the correct params', async function () {
255 await makePostBodyRequest({ 334 await makePostBodyRequest({
256 url: server.url, 335 url: server.url,
@@ -468,11 +547,6 @@ describe('Test users API validators', function () {
468 }) 547 })
469 548
470 describe('When getting a user', function () { 549 describe('When getting a user', function () {
471 before(async function () {
472 const res = await getUsersList(server.url, server.accessToken)
473
474 userId = res.body.data[1].id
475 })
476 550
477 it('Should fail with an non authenticated user', async function () { 551 it('Should fail with an non authenticated user', async function () {
478 await makeGetRequest({ url: server.url, path: path + userId, token: 'super token', statusCodeExpected: 401 }) 552 await makeGetRequest({ url: server.url, path: path + userId, token: 'super token', statusCodeExpected: 401 })
@@ -489,13 +563,6 @@ describe('Test users API validators', function () {
489 563
490 describe('When updating a user', function () { 564 describe('When updating a user', function () {
491 565
492 before(async function () {
493 const res = await getUsersList(server.url, server.accessToken)
494
495 userId = res.body.data[1].id
496 rootId = res.body.data[2].id
497 })
498
499 it('Should fail with an invalid email attribute', async function () { 566 it('Should fail with an invalid email attribute', async function () {
500 const fields = { 567 const fields = {
501 email: 'blabla' 568 email: 'blabla'
@@ -565,7 +632,35 @@ describe('Test users API validators', function () {
565 it('Should fail with invalid admin flags', async function () { 632 it('Should fail with invalid admin flags', async function () {
566 const fields = { adminFlags: 'toto' } 633 const fields = { adminFlags: 'toto' }
567 634
568 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 635 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
636 })
637
638 it('Should fail to update an admin with a moderator', async function () {
639 const fields = {
640 videoQuota: 42
641 }
642
643 await makePutBodyRequest({
644 url: server.url,
645 path: path + moderatorId,
646 token: moderatorAccessToken,
647 fields,
648 statusCodeExpected: 403
649 })
650 })
651
652 it('Should succeed to update a user with a moderator', async function () {
653 const fields = {
654 videoQuota: 42
655 }
656
657 await makePutBodyRequest({
658 url: server.url,
659 path: path + userId,
660 token: moderatorAccessToken,
661 fields,
662 statusCodeExpected: 204
663 })
569 }) 664 })
570 665
571 it('Should succeed with the correct params', async function () { 666 it('Should succeed with the correct params', async function () {
@@ -664,6 +759,17 @@ describe('Test users API validators', function () {
664 await blockUser(server.url, userId, userAccessToken, 403) 759 await blockUser(server.url, userId, userAccessToken, 403)
665 await unblockUser(server.url, userId, userAccessToken, 403) 760 await unblockUser(server.url, userId, userAccessToken, 403)
666 }) 761 })
762
763 it('Should fail on a moderator with a moderator', async function () {
764 await removeUser(server.url, moderatorId, moderatorAccessToken, 403)
765 await blockUser(server.url, moderatorId, moderatorAccessToken, 403)
766 await unblockUser(server.url, moderatorId, moderatorAccessToken, 403)
767 })
768
769 it('Should succeed on a user with a moderator', async function () {
770 await blockUser(server.url, userId, moderatorAccessToken)
771 await unblockUser(server.url, userId, moderatorAccessToken)
772 })
667 }) 773 })
668 774
669 describe('When deleting our account', function () { 775 describe('When deleting our account', function () {