Add demo mode
Adds a demo mode in the settings.py to prevent certain obvious kinds of vandalism on public openslides testing instances
This commit is contained in:
parent
ccc3e38427
commit
fbf424e570
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@ bower_components/*
|
|||||||
|
|
||||||
# OS3+
|
# OS3+
|
||||||
/server/
|
/server/
|
||||||
|
/haproxy/
|
||||||
|
|
||||||
# Local user data (settings, database, media, search index, static files)
|
# Local user data (settings, database, media, search index, static files)
|
||||||
personal_data/*
|
personal_data/*
|
||||||
|
@ -146,3 +146,7 @@ these requests from "prioritized clients" can be routed to different servers.
|
|||||||
`AUTOUPDATE_DELAY`: The delay to send autoupdates. This feature can be
|
`AUTOUPDATE_DELAY`: The delay to send autoupdates. This feature can be
|
||||||
deactivated by setting it to `None`. It is deactivated per default. The Delay is
|
deactivated by setting it to `None`. It is deactivated per default. The Delay is
|
||||||
given in seconds
|
given in seconds
|
||||||
|
|
||||||
|
`DEMO`: Apply special settings for demo use cases. A list of protected user ids handlers
|
||||||
|
to be given. Updating these users (also password) is not allowed. Some bulk actions like
|
||||||
|
resetting password are completly disabled. Irrelevant for normal use cases.
|
13
client/src/app/core/definitions/custom-errors.ts
Normal file
13
client/src/app/core/definitions/custom-errors.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define custom error classes here
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class PreventedInDemo extends Error {
|
||||||
|
public constructor(message: string = _('Cannot do that in demo mode!'), name: string = 'Error') {
|
||||||
|
super(message);
|
||||||
|
this.name = name;
|
||||||
|
Object.setPrototypeOf(this, PreventedInDemo.prototype);
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,11 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||||
import { HttpService } from 'app/core/core-services/http.service';
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { PreventedInDemo } from 'app/core/definitions/custom-errors';
|
||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { NewEntry } from 'app/core/ui-services/base-import.service';
|
import { NewEntry } from 'app/core/ui-services/base-import.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
@ -53,6 +55,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
*/
|
*/
|
||||||
protected sortProperty: SortProperty;
|
protected sortProperty: SortProperty;
|
||||||
|
|
||||||
|
private demoModeUserIds: number[] | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the user repo
|
* Constructor for the user repo
|
||||||
*
|
*
|
||||||
@ -71,7 +75,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
relationManager: RelationManagerService,
|
relationManager: RelationManagerService,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
private httpService: HttpService,
|
private httpService: HttpService,
|
||||||
private configService: ConfigService
|
private configService: ConfigService,
|
||||||
|
private constantsService: ConstantsService
|
||||||
) {
|
) {
|
||||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, User, UserRelations);
|
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, User, UserRelations);
|
||||||
this.sortProperty = this.configService.instant('users_sort_by');
|
this.sortProperty = this.configService.instant('users_sort_by');
|
||||||
@ -79,6 +84,11 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
this.sortProperty = conf;
|
this.sortProperty = conf;
|
||||||
this.setConfigSortFn();
|
this.setConfigSortFn();
|
||||||
});
|
});
|
||||||
|
this.constantsService.get<any>('Settings').subscribe(settings => {
|
||||||
|
if (settings) {
|
||||||
|
this.demoModeUserIds = settings.DEMO || null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTitle = (titleInformation: UserTitleInformation) => {
|
public getTitle = (titleInformation: UserTitleInformation) => {
|
||||||
@ -181,6 +191,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
* @param updateDefaultPassword Control, if the default password should be updated.
|
* @param updateDefaultPassword Control, if the default password should be updated.
|
||||||
*/
|
*/
|
||||||
public async resetPassword(user: ViewUser, password: string): Promise<void> {
|
public async resetPassword(user: ViewUser, password: string): Promise<void> {
|
||||||
|
this.preventAlterationOnDemoUsers(user);
|
||||||
const path = `/rest/users/user/${user.id}/reset_password/`;
|
const path = `/rest/users/user/${user.id}/reset_password/`;
|
||||||
await this.httpService.post(path, { password: password });
|
await this.httpService.post(path, { password: password });
|
||||||
}
|
}
|
||||||
@ -191,7 +202,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
* @param oldPassword the old password
|
* @param oldPassword the old password
|
||||||
* @param newPassword the new password
|
* @param newPassword the new password
|
||||||
*/
|
*/
|
||||||
public async setNewPassword(oldPassword: string, newPassword: string): Promise<void> {
|
public async setNewPassword(user: ViewUser, oldPassword: string, newPassword: string): Promise<void> {
|
||||||
|
this.preventAlterationOnDemoUsers(user);
|
||||||
await this.httpService.post(`${environment.urlPrefix}/users/setpassword/`, {
|
await this.httpService.post(`${environment.urlPrefix}/users/setpassword/`, {
|
||||||
old_password: oldPassword,
|
old_password: oldPassword,
|
||||||
new_password: newPassword
|
new_password: newPassword
|
||||||
@ -205,6 +217,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
* @param users The users to reset the passwords from
|
* @param users The users to reset the passwords from
|
||||||
*/
|
*/
|
||||||
public async bulkResetPasswordsToDefault(users: ViewUser[]): Promise<void> {
|
public async bulkResetPasswordsToDefault(users: ViewUser[]): Promise<void> {
|
||||||
|
this.preventInDemo();
|
||||||
await this.httpService.post('/rest/users/user/bulk_reset_passwords_to_default/', {
|
await this.httpService.post('/rest/users/user/bulk_reset_passwords_to_default/', {
|
||||||
user_ids: users.map(user => user.id)
|
user_ids: users.map(user => user.id)
|
||||||
});
|
});
|
||||||
@ -217,6 +230,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
* @param users The users to generate new passwords for
|
* @param users The users to generate new passwords for
|
||||||
*/
|
*/
|
||||||
public async bulkGenerateNewPasswords(users: ViewUser[]): Promise<void> {
|
public async bulkGenerateNewPasswords(users: ViewUser[]): Promise<void> {
|
||||||
|
this.preventInDemo();
|
||||||
await this.httpService.post('/rest/users/user/bulk_generate_passwords/', {
|
await this.httpService.post('/rest/users/user/bulk_generate_passwords/', {
|
||||||
user_ids: users.map(user => user.id)
|
user_ids: users.map(user => user.id)
|
||||||
});
|
});
|
||||||
@ -234,12 +248,23 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
return await this.httpService.post<MassImportResult>(`/rest/users/user/mass_import/`, { users: data });
|
return await this.httpService.post<MassImportResult>(`/rest/users/user/mass_import/`, { users: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async update(update: Partial<User>, viewModel: ViewUser): Promise<void> {
|
||||||
|
this.preventAlterationOnDemoUsers(viewModel);
|
||||||
|
return super.update(update, viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(viewModel: ViewUser): Promise<void> {
|
||||||
|
this.preventInDemo();
|
||||||
|
return super.delete(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes many users. The operator will not be deleted (if included in `uisers`)
|
* Deletes many users. The operator will not be deleted (if included in `uisers`)
|
||||||
*
|
*
|
||||||
* @param users The users to delete
|
* @param users The users to delete
|
||||||
*/
|
*/
|
||||||
public async bulkDelete(users: ViewUser[]): Promise<void> {
|
public async bulkDelete(users: ViewUser[]): Promise<void> {
|
||||||
|
this.preventInDemo();
|
||||||
await this.httpService.post('/rest/users/user/bulk_delete/', { user_ids: users.map(user => user.id) });
|
await this.httpService.post('/rest/users/user/bulk_delete/', { user_ids: users.map(user => user.id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +280,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
field: 'is_active' | 'is_present' | 'is_committee',
|
field: 'is_active' | 'is_present' | 'is_committee',
|
||||||
value: boolean
|
value: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
this.preventAlterationOnDemoUsers(users);
|
||||||
await this.httpService.post('/rest/users/user/bulk_set_state/', {
|
await this.httpService.post('/rest/users/user/bulk_set_state/', {
|
||||||
user_ids: users.map(user => user.id),
|
user_ids: users.map(user => user.id),
|
||||||
field: field,
|
field: field,
|
||||||
@ -270,6 +296,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
* @param groupIds All group ids to add or remove
|
* @param groupIds All group ids to add or remove
|
||||||
*/
|
*/
|
||||||
public async bulkAlterGroups(users: ViewUser[], action: 'add' | 'remove', groupIds: number[]): Promise<void> {
|
public async bulkAlterGroups(users: ViewUser[], action: 'add' | 'remove', groupIds: number[]): Promise<void> {
|
||||||
|
this.preventAlterationOnDemoUsers(users);
|
||||||
await this.httpService.post('/rest/users/user/bulk_alter_groups/', {
|
await this.httpService.post('/rest/users/user/bulk_alter_groups/', {
|
||||||
user_ids: users.map(user => user.id),
|
user_ids: users.map(user => user.id),
|
||||||
action: action,
|
action: action,
|
||||||
@ -285,6 +312,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
* @param users All affected users
|
* @param users All affected users
|
||||||
*/
|
*/
|
||||||
public async bulkSendInvitationEmail(users: ViewUser[]): Promise<string> {
|
public async bulkSendInvitationEmail(users: ViewUser[]): Promise<string> {
|
||||||
|
this.preventInDemo();
|
||||||
const user_ids = users.map(user => user.id);
|
const user_ids = users.map(user => user.id);
|
||||||
const users_email_subject = this.configService.instant<string>('users_email_subject');
|
const users_email_subject = this.configService.instant<string>('users_email_subject');
|
||||||
const users_email_body = this.configService.instant<string>('users_email_body');
|
const users_email_body = this.configService.instant<string>('users_email_body');
|
||||||
@ -465,4 +493,20 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
}
|
}
|
||||||
return new Date(user.user.last_email_send).toLocaleString(this.translate.currentLang);
|
return new Date(user.user.last_email_send).toLocaleString(this.translate.currentLang);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private preventAlterationOnDemoUsers(users: ViewUser | ViewUser[]): void {
|
||||||
|
if (Array.isArray(users)) {
|
||||||
|
if (users.map(user => user.id).intersect(this.demoModeUserIds).length > 0) {
|
||||||
|
this.preventInDemo();
|
||||||
|
}
|
||||||
|
} else if (this.demoModeUserIds.some(userId => userId === users.id)) {
|
||||||
|
this.preventInDemo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private preventInDemo(): void {
|
||||||
|
if (this.demoModeUserIds) {
|
||||||
|
throw new PreventedInDemo();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ export class PasswordComponent extends BaseViewComponent implements OnInit {
|
|||||||
if (newPassword1 !== newPassword2) {
|
if (newPassword1 !== newPassword2) {
|
||||||
this.raiseError(this.translate.instant('Error: The new passwords do not match.'));
|
this.raiseError(this.translate.instant('Error: The new passwords do not match.'));
|
||||||
} else {
|
} else {
|
||||||
await this.repo.setNewPassword(oldPassword, newPassword1);
|
await this.repo.setNewPassword(this.user, oldPassword, newPassword1);
|
||||||
this.router.navigate(['./']);
|
this.router.navigate(['./']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,6 +133,7 @@ class CoreAppConfig(AppConfig):
|
|||||||
"JITSI_DOMAIN",
|
"JITSI_DOMAIN",
|
||||||
"JITSI_ROOM_NAME",
|
"JITSI_ROOM_NAME",
|
||||||
"JITSI_ROOM_PASSWORD",
|
"JITSI_ROOM_PASSWORD",
|
||||||
|
"DEMO",
|
||||||
]
|
]
|
||||||
client_settings_dict = {}
|
client_settings_dict = {}
|
||||||
for key in client_settings_keys:
|
for key in client_settings_keys:
|
||||||
|
@ -23,6 +23,7 @@ from django.utils.encoding import force_bytes, force_text
|
|||||||
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||||
|
|
||||||
from openslides.saml import SAML_ENABLED
|
from openslides.saml import SAML_ENABLED
|
||||||
|
from openslides.utils import logging
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..core.signals import permission_change
|
from ..core.signals import permission_change
|
||||||
@ -55,7 +56,23 @@ from .serializers import GroupSerializer, PermissionRelatedField
|
|||||||
from .user_backend import user_backend_manager
|
from .user_backend import user_backend_manager
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
demo_mode_users = getattr(settings, "DEMO", None)
|
||||||
|
is_demo_mode = isinstance(demo_mode_users, list) and len(demo_mode_users) > 0
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
if is_demo_mode:
|
||||||
|
logger.info("OpenSlides started in demo mode. Some features are unavailable.")
|
||||||
|
|
||||||
|
|
||||||
|
def assertNoDemoAndAdmin(user_ids):
|
||||||
|
if isinstance(user_ids, int):
|
||||||
|
user_ids = [user_ids]
|
||||||
|
if is_demo_mode and any(user_id in demo_mode_users for user_id in user_ids):
|
||||||
|
raise ValidationError({"detail": "Not allowed in demo mode"})
|
||||||
|
|
||||||
|
|
||||||
|
def assertNoDemo():
|
||||||
|
if is_demo_mode:
|
||||||
|
raise ValidationError({"detail": "Not allowed in demo mode"})
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
@ -117,6 +134,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
wants to update himself or is manager.
|
wants to update himself or is manager.
|
||||||
"""
|
"""
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
|
assertNoDemoAndAdmin(user.id)
|
||||||
# Check permissions.
|
# Check permissions.
|
||||||
if (
|
if (
|
||||||
has_perm(self.request.user, "users.can_see_name")
|
has_perm(self.request.user, "users.can_see_name")
|
||||||
@ -165,6 +183,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
|
|
||||||
Ensures that no one can delete himself.
|
Ensures that no one can delete himself.
|
||||||
"""
|
"""
|
||||||
|
assertNoDemo()
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
if instance == self.request.user:
|
if instance == self.request.user:
|
||||||
raise ValidationError({"detail": "You can not delete yourself."})
|
raise ValidationError({"detail": "You can not delete yourself."})
|
||||||
@ -178,6 +197,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
Expected data: { pasword: <the new password> }
|
Expected data: { pasword: <the new password> }
|
||||||
"""
|
"""
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
|
assertNoDemoAndAdmin(user.id)
|
||||||
if user.auth_type != "default":
|
if user.auth_type != "default":
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
@ -204,6 +224,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
and the default password will be set to the new generated passwords.
|
and the default password will be set to the new generated passwords.
|
||||||
Expected data: { user_ids: <list of ids> }
|
Expected data: { user_ids: <list of ids> }
|
||||||
"""
|
"""
|
||||||
|
assertNoDemo()
|
||||||
ids = request.data.get("user_ids")
|
ids = request.data.get("user_ids")
|
||||||
self.assert_list_of_ints(ids)
|
self.assert_list_of_ints(ids)
|
||||||
|
|
||||||
@ -223,6 +244,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
request user is excluded.
|
request user is excluded.
|
||||||
Expected data: { user_ids: <list of ids> }
|
Expected data: { user_ids: <list of ids> }
|
||||||
"""
|
"""
|
||||||
|
assertNoDemo()
|
||||||
ids = request.data.get("user_ids")
|
ids = request.data.get("user_ids")
|
||||||
self.assert_list_of_ints(ids)
|
self.assert_list_of_ints(ids)
|
||||||
|
|
||||||
@ -260,9 +282,9 @@ class UserViewSet(ModelViewSet):
|
|||||||
value: True|False
|
value: True|False
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ids = request.data.get("user_ids")
|
ids = request.data.get("user_ids")
|
||||||
self.assert_list_of_ints(ids)
|
self.assert_list_of_ints(ids)
|
||||||
|
assertNoDemoAndAdmin(ids)
|
||||||
|
|
||||||
field = request.data.get("field")
|
field = request.data.get("field")
|
||||||
if field not in ("is_active", "is_present", "is_committee"):
|
if field not in ("is_active", "is_present", "is_committee"):
|
||||||
@ -293,6 +315,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
user_ids = request.data.get("user_ids")
|
user_ids = request.data.get("user_ids")
|
||||||
self.assert_list_of_ints(user_ids)
|
self.assert_list_of_ints(user_ids)
|
||||||
|
assertNoDemoAndAdmin(user_ids)
|
||||||
group_ids = request.data.get("group_ids")
|
group_ids = request.data.get("group_ids")
|
||||||
self.assert_list_of_ints(group_ids, ids_name="groups_id")
|
self.assert_list_of_ints(group_ids, ids_name="groups_id")
|
||||||
|
|
||||||
@ -318,6 +341,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
Deletes many users. The request user will be excluded. Expected data:
|
Deletes many users. The request user will be excluded. Expected data:
|
||||||
{ user_ids: <list of ids> }
|
{ user_ids: <list of ids> }
|
||||||
"""
|
"""
|
||||||
|
assertNoDemo()
|
||||||
ids = request.data.get("user_ids")
|
ids = request.data.get("user_ids")
|
||||||
self.assert_list_of_ints(ids)
|
self.assert_list_of_ints(ids)
|
||||||
|
|
||||||
@ -396,6 +420,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
Endpoint to send invitation emails to all given users (by id). Returns the
|
Endpoint to send invitation emails to all given users (by id). Returns the
|
||||||
number of emails send.
|
number of emails send.
|
||||||
"""
|
"""
|
||||||
|
assertNoDemo()
|
||||||
user_ids = request.data.get("user_ids")
|
user_ids = request.data.get("user_ids")
|
||||||
self.assert_list_of_ints(user_ids)
|
self.assert_list_of_ints(user_ids)
|
||||||
# Get subject and body from the response. Do not use the config values
|
# Get subject and body from the response. Do not use the config values
|
||||||
@ -881,6 +906,7 @@ class SetPasswordView(APIView):
|
|||||||
or user.auth_type != "default"
|
or user.auth_type != "default"
|
||||||
):
|
):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
|
assertNoDemoAndAdmin(user.id)
|
||||||
if user.check_password(request.data["old_password"]):
|
if user.check_password(request.data["old_password"]):
|
||||||
try:
|
try:
|
||||||
validate_password(request.data.get("new_password"), user=user)
|
validate_password(request.data.get("new_password"), user=user)
|
||||||
|
Loading…
Reference in New Issue
Block a user