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+
/server/
/haproxy/
# Local user data (settings, database, media, search index, static files)
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
deactivated by setting it to `None`. It is deactivated per default. The Delay is
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 { ConstantsService } from 'app/core/core-services/constants.service';
import { HttpService } from 'app/core/core-services/http.service';
import { RelationManagerService } from 'app/core/core-services/relation-manager.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 { NewEntry } from 'app/core/ui-services/base-import.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;
private demoModeUserIds: number[] | null = null;
/**
* Constructor for the user repo
*
@ -71,7 +75,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
relationManager: RelationManagerService,
protected translate: TranslateService,
private httpService: HttpService,
private configService: ConfigService
private configService: ConfigService,
private constantsService: ConstantsService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, User, UserRelations);
this.sortProperty = this.configService.instant('users_sort_by');
@ -79,6 +84,11 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
this.sortProperty = conf;
this.setConfigSortFn();
});
this.constantsService.get<any>('Settings').subscribe(settings => {
if (settings) {
this.demoModeUserIds = settings.DEMO || null;
}
});
}
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.
*/
public async resetPassword(user: ViewUser, password: string): Promise<void> {
this.preventAlterationOnDemoUsers(user);
const path = `/rest/users/user/${user.id}/reset_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 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/`, {
old_password: oldPassword,
new_password: newPassword
@ -205,6 +217,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
* @param users The users to reset the passwords from
*/
public async bulkResetPasswordsToDefault(users: ViewUser[]): Promise<void> {
this.preventInDemo();
await this.httpService.post('/rest/users/user/bulk_reset_passwords_to_default/', {
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
*/
public async bulkGenerateNewPasswords(users: ViewUser[]): Promise<void> {
this.preventInDemo();
await this.httpService.post('/rest/users/user/bulk_generate_passwords/', {
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 });
}
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`)
*
* @param users The users to delete
*/
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) });
}
@ -255,6 +280,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
field: 'is_active' | 'is_present' | 'is_committee',
value: boolean
): Promise<void> {
this.preventAlterationOnDemoUsers(users);
await this.httpService.post('/rest/users/user/bulk_set_state/', {
user_ids: users.map(user => user.id),
field: field,
@ -270,6 +296,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
* @param groupIds All group ids to add or remove
*/
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/', {
user_ids: users.map(user => user.id),
action: action,
@ -285,6 +312,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
* @param users All affected users
*/
public async bulkSendInvitationEmail(users: ViewUser[]): Promise<string> {
this.preventInDemo();
const user_ids = users.map(user => user.id);
const users_email_subject = this.configService.instant<string>('users_email_subject');
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);
}
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) {
this.raiseError(this.translate.instant('Error: The new passwords do not match.'));
} else {
await this.repo.setNewPassword(oldPassword, newPassword1);
await this.repo.setNewPassword(this.user, oldPassword, newPassword1);
this.router.navigate(['./']);
}
}

View File

@ -133,6 +133,7 @@ class CoreAppConfig(AppConfig):
"JITSI_DOMAIN",
"JITSI_ROOM_NAME",
"JITSI_ROOM_PASSWORD",
"DEMO",
]
client_settings_dict = {}
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 openslides.saml import SAML_ENABLED
from openslides.utils import logging
from ..core.config import config
from ..core.signals import permission_change
@ -55,7 +56,23 @@ from .serializers import GroupSerializer, PermissionRelatedField
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):
@ -117,6 +134,7 @@ class UserViewSet(ModelViewSet):
wants to update himself or is manager.
"""
user = self.get_object()
assertNoDemoAndAdmin(user.id)
# Check permissions.
if (
has_perm(self.request.user, "users.can_see_name")
@ -165,6 +183,7 @@ class UserViewSet(ModelViewSet):
Ensures that no one can delete himself.
"""
assertNoDemo()
instance = self.get_object()
if instance == self.request.user:
raise ValidationError({"detail": "You can not delete yourself."})
@ -178,6 +197,7 @@ class UserViewSet(ModelViewSet):
Expected data: { pasword: <the new password> }
"""
user = self.get_object()
assertNoDemoAndAdmin(user.id)
if user.auth_type != "default":
raise ValidationError(
{
@ -204,6 +224,7 @@ class UserViewSet(ModelViewSet):
and the default password will be set to the new generated passwords.
Expected data: { user_ids: <list of ids> }
"""
assertNoDemo()
ids = request.data.get("user_ids")
self.assert_list_of_ints(ids)
@ -223,6 +244,7 @@ class UserViewSet(ModelViewSet):
request user is excluded.
Expected data: { user_ids: <list of ids> }
"""
assertNoDemo()
ids = request.data.get("user_ids")
self.assert_list_of_ints(ids)
@ -260,9 +282,9 @@ class UserViewSet(ModelViewSet):
value: True|False
}
"""
ids = request.data.get("user_ids")
self.assert_list_of_ints(ids)
assertNoDemoAndAdmin(ids)
field = request.data.get("field")
if field not in ("is_active", "is_present", "is_committee"):
@ -293,6 +315,7 @@ class UserViewSet(ModelViewSet):
"""
user_ids = request.data.get("user_ids")
self.assert_list_of_ints(user_ids)
assertNoDemoAndAdmin(user_ids)
group_ids = request.data.get("group_ids")
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:
{ user_ids: <list of ids> }
"""
assertNoDemo()
ids = request.data.get("user_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
number of emails send.
"""
assertNoDemo()
user_ids = request.data.get("user_ids")
self.assert_list_of_ints(user_ids)
# 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"
):
self.permission_denied(request)
assertNoDemoAndAdmin(user.id)
if user.check_password(request.data["old_password"]):
try:
validate_password(request.data.get("new_password"), user=user)