diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-30 09:59:19 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-07-30 09:59:19 +0200 |
commit | a95a4cc89155f448e6f9ca0957170f3c72a9d964 (patch) | |
tree | f390fa3eccef0991db5694ef58d6716228a7f67a | |
parent | dc8902634864841be7ca483b8e1c0f5afa609c32 (diff) | |
download | PeerTube-a95a4cc89155f448e6f9ca0957170f3c72a9d964.tar.gz PeerTube-a95a4cc89155f448e6f9ca0957170f3c72a9d964.tar.zst PeerTube-a95a4cc89155f448e6f9ca0957170f3c72a9d964.zip |
Moderators can only manage users
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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { Notifier, ServerService } from '@app/core' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { UserCreate, UserRole } from '../../../../../../shared' | 4 | import { UserCreate, UserRole } from '../../../../../../shared' |
5 | import { UserEdit } from './user-edit' | 5 | import { UserEdit } from './user-edit' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -8,7 +8,6 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val | |||
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
9 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 9 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
10 | import { UserService } from '@app/shared' | 10 | import { UserService } from '@app/shared' |
11 | import { 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 @@ | |||
1 | import { ServerService } from '../../../core' | 1 | import { AuthService, ServerService } from '../../../core' |
2 | import { FormReactive } from '../../../shared' | 2 | import { FormReactive } from '../../../shared' |
3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' | 3 | import { USER_ROLE_LABELS, UserRole, VideoResolution } from '../../../../../../shared' |
4 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 4 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
5 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 5 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
6 | 6 | ||
7 | export abstract class UserEdit extends FormReactive { | 7 | export 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 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Subscription } from 'rxjs' | 3 | import { Subscription } from 'rxjs' |
4 | import { Notifier } from '@app/core' | 4 | import { AuthService, Notifier } from '@app/core' |
5 | import { ServerService } from '../../../core' | 5 | import { ServerService } from '../../../core' |
6 | import { UserEdit } from './user-edit' | 6 | import { UserEdit } from './user-edit' |
7 | import { User, UserUpdate } from '../../../../../../shared' | 7 | import { 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 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { AuthService, Notifier } from '@app/core' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | import { ConfirmService, ServerService } from '../../../core' | 4 | import { ConfirmService, ServerService } from '../../../core' |
5 | import { RestPagination, RestTable, UserService } from '../../../shared' | 5 | import { 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' |
36 | import { UserModel } from '../../../models/account/user' | 37 | import { UserModel } from '../../../models/account/user' |
37 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 38 | import { 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 | ) |
102 | usersRouter.post('/:id/unblock', | 104 | usersRouter.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' | |||
30 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 30 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
31 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | 31 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' |
32 | import { doesVideoExist } from '../../helpers/middlewares' | 32 | import { doesVideoExist } from '../../helpers/middlewares' |
33 | import { UserRole } from '../../../shared/models/users' | ||
33 | 34 | ||
34 | const usersAddValidator = [ | 35 | const 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 | ||
395 | const 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 | ||
402 | export { | 410 | export { |
@@ -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 @@ | |||
3 | import { omit } from 'lodash' | 3 | import { omit } from 'lodash' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { UserRole, VideoImport, VideoImportState } from '../../../../shared' | 6 | import { User, UserRole, VideoImport, VideoImportState } from '../../../../shared' |
7 | 7 | ||
8 | import { | 8 | import { |
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 () { |