diff --git a/.gitignore b/.gitignore index 2d3f5be56..19c7bd08b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ bower_components/* # OS3+ /server/ +/haproxy/ # Local user data (settings, database, media, search index, static files) personal_data/* diff --git a/SETTINGS.rst b/SETTINGS.rst index 99fa2c0af..90eed3c2e 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -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. \ No newline at end of file diff --git a/client/src/app/core/definitions/custom-errors.ts b/client/src/app/core/definitions/custom-errors.ts new file mode 100644 index 000000000..7c17abbc3 --- /dev/null +++ b/client/src/app/core/definitions/custom-errors.ts @@ -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); + } +} diff --git a/client/src/app/core/repositories/users/user-repository.service.ts b/client/src/app/core/repositories/users/user-repository.service.ts index db2f865b7..d7003e6ad 100644 --- a/client/src/app/core/repositories/users/user-repository.service.ts +++ b/client/src/app/core/repositories/users/user-repository.service.ts @@ -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('Settings').subscribe(settings => { + if (settings) { + this.demoModeUserIds = settings.DEMO || null; + } + }); } public getTitle = (titleInformation: UserTitleInformation) => { @@ -181,6 +191,7 @@ export class UserRepositoryService extends BaseRepository { + 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 { + public async setNewPassword(user: ViewUser, oldPassword: string, newPassword: string): Promise { + 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 { + 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 { + 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(`/rest/users/user/mass_import/`, { users: data }); } + public async update(update: Partial, viewModel: ViewUser): Promise { + this.preventAlterationOnDemoUsers(viewModel); + return super.update(update, viewModel); + } + + public async delete(viewModel: ViewUser): Promise { + 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 { + 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 { + 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 { + 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 { + this.preventInDemo(); const user_ids = users.map(user => user.id); const users_email_subject = this.configService.instant('users_email_subject'); const users_email_body = this.configService.instant('users_email_body'); @@ -465,4 +493,20 @@ export class UserRepositoryService extends BaseRepository 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(); + } + } } diff --git a/client/src/app/site/users/components/password/password.component.ts b/client/src/app/site/users/components/password/password.component.ts index 935415ece..26660e028 100644 --- a/client/src/app/site/users/components/password/password.component.ts +++ b/client/src/app/site/users/components/password/password.component.ts @@ -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(['./']); } } diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 1899e24dc..5d993e3c2 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -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: diff --git a/openslides/users/views.py b/openslides/users/views.py index b12dcf723..1361f8da5 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -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: } """ 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: } """ + 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: } """ + 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: } """ + 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)