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:
Sean 2020-08-12 11:05:31 +02:00 committed by Finn Stutzenstein
parent ccc3e38427
commit fbf424e570
No known key found for this signature in database
GPG Key ID: 9042F605C6324654
7 changed files with 94 additions and 5 deletions

1
.gitignore vendored
View File

@ -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/*

View File

@ -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.

View 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);
}
}

View File

@ -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();
}
}
} }

View File

@ -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(['./']);
} }
} }

View File

@ -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:

View File

@ -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)