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+
|
||||
/server/
|
||||
/haproxy/
|
||||
|
||||
# Local user data (settings, database, media, search index, static files)
|
||||
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
|
||||
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.
|
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 { 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(['./']);
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user