diff --git a/client/src/app/core/core-services/auth-guard.service.ts b/client/src/app/core/core-services/auth-guard.service.ts index 0127d7d82..b9d894bb8 100644 --- a/client/src/app/core/core-services/auth-guard.service.ts +++ b/client/src/app/core/core-services/auth-guard.service.ts @@ -28,6 +28,10 @@ export class AuthGuard implements CanActivate, CanActivateChild { public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { const basePerm: string | string[] = route.data.basePerm; + console.log('Auth guard'); + console.log('motions.can_see:', this.operator.hasPerms('motions.can_see')); + console.log('motions.can_manage:', this.operator.hasPerms('motions.can_manage')); + if (!basePerm) { return true; } else if (basePerm instanceof Array) { diff --git a/client/src/app/core/core-services/auth.service.ts b/client/src/app/core/core-services/auth.service.ts index 053e729ce..070e0ece6 100644 --- a/client/src/app/core/core-services/auth.service.ts +++ b/client/src/app/core/core-services/auth.service.ts @@ -1,21 +1,12 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; -import { OperatorService } from 'app/core/core-services/operator.service'; +import { OperatorService, WhoAmI } from 'app/core/core-services/operator.service'; import { environment } from 'environments/environment'; -import { User } from '../../shared/models/users/user'; import { OpenSlidesService } from './openslides.service'; import { HttpService } from './http.service'; import { DataStoreService } from './data-store.service'; -/** - * The data returned by a post request to the login route. - */ -interface LoginResponse { - user_id: number; - user: User; -} - /** * Authenticates an OpenSlides user with username and password */ @@ -49,31 +40,57 @@ export class AuthService { * @param password * @returns The login response. */ - public async login(username: string, password: string): Promise { + public async login(username: string, password: string, earlySuccessCallback: () => void): Promise { const user = { username: username, password: password }; - const response = await this.http.post(environment.urlPrefix + '/users/login/', user); + const response = await this.http.post(environment.urlPrefix + '/users/login/', user); + earlySuccessCallback(); + await this.operator.setWhoAmI(response); + await this.redirectUser(response.user_id); return response; } + /** + * Redirects the user to the page where he came from. Boots OpenSlides, + * if it wasn't done before. + */ + public async redirectUser(userId: number): Promise { + if (!this.OpenSlides.booted) { + await this.OpenSlides.afterLoginBootup(userId); + } + + let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/'; + if (redirect.includes('login')) { + redirect = '/'; + } + this.router.navigate([redirect]); + } + + /** + * Login for guests. + */ + public async guestLogin(): Promise { + this.redirectUser(null); + } + /** * Logout function for both the client and the server. * - * Will clear the current operator and datastore and + * Will clear the datastore, update the current operator and * send a `post`-request to `/apps/users/logout/'`. Restarts OpenSlides. */ public async logout(): Promise { - await this.operator.setUser(null); + let response = null; try { - await this.http.post(environment.urlPrefix + '/users/logout/', {}); + response = await this.http.post(environment.urlPrefix + '/users/logout/', {}); } catch (e) { // We do nothing on failures. Reboot OpenSlides anyway. } - // Clear the DataStore - this.DS.clear(); + await this.DS.clear(); + await this.operator.setWhoAmI(response); + await this.OpenSlides.reboot(); this.router.navigate(['/']); - this.OpenSlides.reboot(); } } diff --git a/client/src/app/core/core-services/openslides.service.ts b/client/src/app/core/core-services/openslides.service.ts index 0c11e4251..b4bf63643 100644 --- a/client/src/app/core/core-services/openslides.service.ts +++ b/client/src/app/core/core-services/openslides.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; +import { take } from 'rxjs/operators'; + import { WebsocketService } from './websocket.service'; -import { OperatorService, WhoAmIResponse } from './operator.service'; +import { OperatorService } from './operator.service'; import { StorageService } from './storage.service'; import { AutoupdateService } from './autoupdate.service'; import { DataStoreService } from './data-store.service'; -import { take } from 'rxjs/operators'; /** * Handles the bootup/showdown of this application. @@ -16,10 +17,20 @@ import { take } from 'rxjs/operators'; }) export class OpenSlidesService { /** - * if the user tries to access a certain URL without being authenticated, the URL will be stored here + * If the user tries to access a certain URL without being authenticated, the URL will be stored here */ public redirectUrl: string; + /** + * Saves, if OpenSlides is fully booted. This means, that a user must be logged in + * (Anonymous is also a user in this case). This is the case after `afterLoginBootup`. + */ + private _booted = false; + + public get booted(): boolean { + return this._booted; + } + /** * Constructor to create the OpenSlidesService. Registers itself to the WebsocketService. * @param storageService @@ -53,10 +64,6 @@ export class OpenSlidesService { public async bootup(): Promise { // start autoupdate if the user is logged in: const response = await this.operator.whoAmIFromStorage(); - await this.bootupWithWhoAmI(response); - } - - private async bootupWithWhoAmI(response: WhoAmIResponse): Promise { if (!response.user && !response.guest_enabled) { this.redirectUrl = location.pathname; @@ -85,9 +92,10 @@ export class OpenSlidesService { * the login bootup-sequence: Check (and maybe clear) the cache und setup the DataStore * and websocket. This "login" also may be the "login" of an anonymous when he is using * OpenSlides as a guest. - * @param userId + * @param userId the id or null for guest */ - public async afterLoginBootup(userId: number): Promise { + public async afterLoginBootup(userId: number | null): Promise { + console.log('user id', userId); // Check, which user was logged in last time const lastUserId = await this.storageService.get('lastUserLoggedIn'); // if the user changed, reset the cache and save the new user. @@ -96,6 +104,8 @@ export class OpenSlidesService { await this.storageService.set('lastUserLoggedIn', userId); } await this.setupDataStoreAndWebSocket(); + // Now finally booted. + this._booted = true; } /** @@ -121,16 +131,16 @@ export class OpenSlidesService { /** * Shuts OpenSlides down. The websocket is closed and the operator is not set. */ - public async shutdown(): Promise { + public shutdown(): void { this.websocketService.close(); - await this.operator.setUser(null); + this._booted = false; } /** * Shutdown and bootup. */ public async reboot(): Promise { - await this.shutdown(); + this.shutdown(); await this.bootup(); } diff --git a/client/src/app/core/core-services/operator.service.ts b/client/src/app/core/core-services/operator.service.ts index ecd203d4f..606e4d4de 100644 --- a/client/src/app/core/core-services/operator.service.ts +++ b/client/src/app/core/core-services/operator.service.ts @@ -13,6 +13,7 @@ import { UserRepositoryService } from '../repositories/users/user-repository.ser import { CollectionStringMapperService } from './collectionStringMapper.service'; import { StorageService } from './storage.service'; import { HttpService } from './http.service'; +import { filter, auditTime } from 'rxjs/operators'; /** * Permissions on the client are just strings. This makes clear, that @@ -23,18 +24,24 @@ export type Permission = string; /** * Response format of the WhoAmI request. */ -export interface WhoAmIResponse { +export interface WhoAmI { user_id: number; guest_enabled: boolean; user: User; + permissions: Permission[]; } -function isWhoAmIResponse(obj: any): obj is WhoAmIResponse { +function isWhoAmI(obj: any): obj is WhoAmI { if (!obj) { return false; } - const whoAmI = obj as WhoAmIResponse; - return whoAmI.guest_enabled !== undefined && whoAmI.user !== undefined && whoAmI.user_id !== undefined; + const whoAmI = obj as WhoAmI; + return ( + whoAmI.guest_enabled !== undefined && + whoAmI.user !== undefined && + whoAmI.user_id !== undefined && + whoAmI.permissions !== undefined + ); } const WHOAMI_STORAGE_KEY = 'whoami'; @@ -80,7 +87,7 @@ export class OperatorService implements OnAfterAppsLoaded { * Save, if guests are enabled. */ public get guestsEnabled(): boolean { - return this.currentWhoAmIResponse ? this.currentWhoAmIResponse.guest_enabled : false; + return this.currentWhoAmI ? this.currentWhoAmI.guest_enabled : false; } /** @@ -106,7 +113,7 @@ export class OperatorService implements OnAfterAppsLoaded { /** * The current WhoAmI response to extract the user (the operator) from. */ - private currentWhoAmIResponse: WhoAmIResponse | null; + private currentWhoAmI: WhoAmI | null; /** * Sets up an observer for watching changes in the DS. If the operator user or groups are changed, @@ -124,43 +131,51 @@ export class OperatorService implements OnAfterAppsLoaded { private storageService: StorageService ) { this.DS.changeObservable.subscribe(newModel => { - if (this._user) { - if (newModel instanceof Group) { - this.updatePermissions(); - } - - if (newModel instanceof User && this._user.id === newModel.id) { - this.updateUser(newModel); - } - } else if (newModel instanceof Group && newModel.id === 1) { - // Group 1 (default) for anonymous changed - this.updatePermissions(); + if (this._user && newModel instanceof User && this._user.id === newModel.id) { + this._user = newModel; + this.updateUserInCurrentWhoAmI(); } }); + this.DS.changeObservable + .pipe( + filter( + model => + // Any group has changed if we have an operator or + // group 1 (default) for anonymous changed + model instanceof Group && (!!this._user || model.id === 1) + ), + auditTime(10) + ) + .subscribe(newModel => this.updatePermissions()); } /** - * Gets the current WHoAmI response from the storage. + * Returns a default WhoAmI response */ - public async whoAmIFromStorage(): Promise { - const defaultResponse = { + private getDefaultWhoAmIResponse(): WhoAmI { + return { user_id: null, guest_enabled: false, - user: null + user: null, + permissions: [] }; - let response: WhoAmIResponse; + } + + /** + * Gets the current WhoAmI response from the storage. + */ + public async whoAmIFromStorage(): Promise { + let response: WhoAmI; try { - response = await this.storageService.get(WHOAMI_STORAGE_KEY); - if (response) { - this.processWhoAmIResponse(response); - } else { - response = defaultResponse; + response = await this.storageService.get(WHOAMI_STORAGE_KEY); + if (!response) { + response = this.getDefaultWhoAmIResponse(); } } catch (e) { - response = defaultResponse; + response = this.getDefaultWhoAmIResponse(); } - this.currentWhoAmIResponse = response; - return this.currentWhoAmIResponse; + await this.updateCurrentWhoAmI(response); + return this.currentWhoAmI; } /** @@ -177,27 +192,11 @@ export class OperatorService implements OnAfterAppsLoaded { * Sets the operator user. Will be saved to storage * @param user The new operator. */ - public async setUser(user: User): Promise { - await this.updateUser(user, true); - } - - /** - * Updates the user and update the permissions. - * - * @param user The user to set. - * @param saveToStoare Whether to save the user to the storage WhoAmI. - */ - private async updateUser(user: User | null, saveToStorage: boolean = false): Promise { - this._user = user; - if (saveToStorage) { - await this.saveUserToStorate(); + public async setWhoAmI(whoami: WhoAmI | null): Promise { + if (whoami === null) { + whoami = this.getDefaultWhoAmIResponse(); } - if (user && this.userRepository) { - this._viewUser = this.userRepository.getViewModel(user.id); - } else { - this._viewUser = null; - } - this.updatePermissions(); + await this.updateCurrentWhoAmI(whoami); } /** @@ -205,51 +204,56 @@ export class OperatorService implements OnAfterAppsLoaded { * * @returns The response of the WhoAmI request. */ - public async whoAmI(): Promise { + public async whoAmI(): Promise { try { const response = await this.http.get(environment.urlPrefix + '/users/whoami/'); - if (isWhoAmIResponse(response)) { - this.processWhoAmIResponse(response); - await this.storageService.set(WHOAMI_STORAGE_KEY, response); - this.currentWhoAmIResponse = response; + if (isWhoAmI(response)) { + await this.updateCurrentWhoAmI(response); } else { this.offlineService.goOfflineBecauseFailedWhoAmI(); } } catch (e) { this.offlineService.goOfflineBecauseFailedWhoAmI(); } - return this.currentWhoAmIResponse; + return this.currentWhoAmI; } /** * Saves the user to storage by wrapping it into a (maybe existing) * WhoAMI response. */ - private async saveUserToStorate(): Promise { - if (!this.currentWhoAmIResponse) { - this.currentWhoAmIResponse = { - user_id: null, - guest_enabled: false, - user: null - }; + private async updateUserInCurrentWhoAmI(): Promise { + if (!this.currentWhoAmI) { + this.currentWhoAmI = this.getDefaultWhoAmIResponse(); } - if (this.user) { - this.currentWhoAmIResponse.user_id = this.user.id; - this.currentWhoAmIResponse.user = this.user; + if (this.isAnonymous) { + this.currentWhoAmI.user_id = null; + this.currentWhoAmI.user = null; } else { - this.currentWhoAmIResponse.user_id = null; - this.currentWhoAmIResponse.user = null; + this.currentWhoAmI.user_id = this.user.id; + this.currentWhoAmI.user = this.user; } - await this.storageService.set(WHOAMI_STORAGE_KEY, this.currentWhoAmIResponse); + this.currentWhoAmI.permissions = this.permissions; + await this.updateCurrentWhoAmI(); } /** - * Processes a WhoAmI response and set the user appropriately. - * - * @param response The WhoAMI response + * Updates the user and update the permissions. */ - private processWhoAmIResponse(response: WhoAmIResponse): void { - this.updateUser(response.user ? new User(response.user) : null); + private async updateCurrentWhoAmI(whoami?: WhoAmI): Promise { + if (whoami) { + this.currentWhoAmI = whoami; + } else { + whoami = this.currentWhoAmI; + } + + this._user = whoami ? whoami.user : null; + if (this._user && this.userRepository) { + this._viewUser = this.userRepository.getViewModel(this._user.id); + } else { + this._viewUser = null; + } + await this.updatePermissions(); } /** @@ -306,24 +310,42 @@ export class OperatorService implements OnAfterAppsLoaded { /** * Update the operators permissions and publish the operator afterwards. + * Saves the current WhoAmI to storage with the updated permissions */ - private updatePermissions(): void { + private async updatePermissions(): Promise { this.permissions = []; - // Anonymous or users in the default group. - if (!this.user || this.user.groups_id.length === 0) { - const defaultGroup = this.DS.get('users/group', 1); - if (defaultGroup && defaultGroup.permissions instanceof Array) { - this.permissions = defaultGroup.permissions; + + // If we do not have any groups, take the permissions from the + // latest WhoAmI response. + if (this.DS.getAll(Group).length === 0) { + if (this.currentWhoAmI) { + this.permissions = this.currentWhoAmI.permissions; } } else { - const permissionSet = new Set(); - this.DS.getMany(Group, this.user.groups_id).forEach(group => { - group.permissions.forEach(permission => { - permissionSet.add(permission); + // Anonymous or users in the default group. + if (!this.user || this.user.groups_id.length === 0) { + const defaultGroup = this.DS.get('users/group', 1); + if (defaultGroup && defaultGroup.permissions instanceof Array) { + this.permissions = defaultGroup.permissions; + } + } else { + const permissionSet = new Set(); + this.DS.getMany(Group, this.user.groups_id).forEach(group => { + group.permissions.forEach(permission => { + permissionSet.add(permission); + }); }); - }); - this.permissions = Array.from(permissionSet.values()); + this.permissions = Array.from(permissionSet.values()); + } } + + // Save perms to current WhoAmI + if (!this.currentWhoAmI) { + this.currentWhoAmI = this.getDefaultWhoAmIResponse(); + } + this.currentWhoAmI.permissions = this.permissions; + await this.storageService.set(WHOAMI_STORAGE_KEY, this.currentWhoAmI); + // publish changes in the operator. this.operatorSubject.next(this.user); this.viewOperatorSubject.next(this.viewUser); diff --git a/client/src/app/site/login/components/login-mask/login-mask.component.ts b/client/src/app/site/login/components/login-mask/login-mask.component.ts index 76c9a4d4b..9492416ad 100644 --- a/client/src/app/site/login/components/login-mask/login-mask.component.ts +++ b/client/src/app/site/login/components/login-mask/login-mask.component.ts @@ -9,11 +9,9 @@ import { BaseComponent } from 'app/base.component'; import { AuthService } from 'app/core/core-services/auth.service'; import { OperatorService } from 'app/core/core-services/operator.service'; import { environment } from 'environments/environment'; -import { OpenSlidesService } from 'app/core/core-services/openslides.service'; import { LoginDataService, LoginData } from 'app/core/ui-services/login-data.service'; import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher'; import { HttpService } from 'app/core/core-services/http.service'; -import { User } from 'app/shared/models/users/user'; interface LoginDataWithInfoText extends LoginData { info_text?: string; @@ -81,7 +79,6 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr private route: ActivatedRoute, private formBuilder: FormBuilder, private httpService: HttpService, - private OpenSlides: OpenSlidesService, private loginDataService: LoginDataService ) { super(); @@ -112,8 +109,7 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr this.operatorSubscription = this.operator.getUserObservable().subscribe(user => { if (user) { this.clearOperatorSubscription(); - this.redirectUser(); - this.OpenSlides.afterLoginBootup(user.id); + this.authService.redirectUser(user.id); } }); } @@ -154,30 +150,16 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr this.loginErrorMsg = ''; this.inProcess = true; try { - const response = await this.authService.login(this.loginForm.value.username, this.loginForm.value.password); - this.clearOperatorSubscription(); // We take control, not the subscription. - this.inProcess = false; - await this.operator.setUser(new User(response.user)); - this.OpenSlides.afterLoginBootup(response.user_id); - this.redirectUser(); + await this.authService.login(this.loginForm.value.username, this.loginForm.value.password, () => { + this.clearOperatorSubscription(); // We take control, not the subscription. + }); } catch (e) { this.loginForm.setErrors({ notFound: true }); this.loginErrorMsg = e; - this.inProcess = false; } - } - - /** - * Redirects the user to the page where he came from. - */ - private redirectUser(): void { - let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/'; - if (redirect.includes('login')) { - redirect = '/'; - } - this.router.navigate([redirect]); + this.inProcess = false; } /** @@ -198,6 +180,6 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr * Guests (if enabled) can navigate directly to the main page. */ public guestLogin(): void { - this.router.navigate(['/']); + this.authService.guestLogin(); } } diff --git a/openslides/users/views.py b/openslides/users/views.py index 516fe7546..baaa8fab2 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -1,6 +1,6 @@ import smtplib import textwrap -from typing import List +from typing import List, Set from asgiref.sync import async_to_sync from django.conf import settings @@ -471,7 +471,46 @@ class PersonalNoteViewSet(ModelViewSet): # Special API views -class UserLoginView(APIView): +class WhoAmIDataView(APIView): + def get_whoami_data(self): + """ + Appends the user id to the context. Uses None for the anonymous + user. Appends also a flag if guest users are enabled in the config. + Appends also the serialized user if available. + """ + user_id = self.request.user.pk or 0 + guest_enabled = anonymous_is_enabled() + + if user_id: + user_data = async_to_sync(element_cache.get_element_restricted_data)( + user_id, self.request.user.get_collection_string(), user_id + ) + group_ids = user_data["groups_id"] or [GROUP_DEFAULT_PK] + else: + user_data = None + group_ids = [GROUP_DEFAULT_PK] if guest_enabled else [] + + # collect all permissions + permissions: Set[str] = set() + group_all_data = async_to_sync(element_cache.get_all_full_data_ordered)()[ + "users/group" + ] + for group_id in group_ids: + permissions.update(group_all_data[group_id]["permissions"]) + + return { + "user_id": user_id or None, + "guest_enabled": guest_enabled, + "user": user_data, + "permissions": list(permissions), + } + + def get_context_data(self, **context): + context.update(self.get_whoami_data()) + return super().get_context_data(**context) + + +class UserLoginView(WhoAmIDataView): """ Login the user. """ @@ -525,14 +564,11 @@ class UserLoginView(APIView): context["theme"] = config["openslides_theme"] else: # self.request.method == 'POST' - context["user_id"] = self.user.pk - context["user"] = async_to_sync(element_cache.get_element_restricted_data)( - self.user.pk or 0, self.user.get_collection_string(), self.user.pk - ) + context.update(self.get_whoami_data()) return super().get_context_data(**context) -class UserLogoutView(APIView): +class UserLogoutView(WhoAmIDataView): """ Logout the user. """ @@ -546,33 +582,13 @@ class UserLogoutView(APIView): return super().post(*args, **kwargs) -class WhoAmIView(APIView): +class WhoAmIView(WhoAmIDataView): """ Returns the id of the requesting user. """ http_method_names = ["get"] - def get_context_data(self, **context): - """ - Appends the user id to the context. Uses None for the anonymous - user. Appends also a flag if guest users are enabled in the config. - Appends also the serialized user if available. - """ - user_id = self.request.user.pk or 0 - if user_id: - user_data = async_to_sync(element_cache.get_element_restricted_data)( - user_id, self.request.user.get_collection_string(), user_id - ) - else: - user_data = None - return super().get_context_data( - user_id=user_id or None, - guest_enabled=anonymous_is_enabled(), - user=user_data, - **context, - ) - class SetPasswordView(APIView): """ diff --git a/tests/integration/users/test_views.py b/tests/integration/users/test_views.py index d7a502068..dcaf0a97f 100644 --- a/tests/integration/users/test_views.py +++ b/tests/integration/users/test_views.py @@ -15,7 +15,7 @@ class TestWhoAmIView(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual( json.loads(response.content.decode()), - {"user_id": None, "user": None, "guest_enabled": False}, + {"user_id": None, "user": None, "permissions": [], "guest_enabled": False}, ) def test_get_authenticated_user(self): @@ -56,6 +56,10 @@ class TestUserLogoutView(TestCase): self.assertEqual(response.status_code, 200) self.assertFalse(hasattr(self.client.session, "test_key")) + self.assertEqual( + json.loads(response.content.decode()), + {"user_id": None, "user": None, "permissions": [], "guest_enabled": False}, + ) class TestUserLoginView(TestCase):