Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New delegator restriction settings #3528

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions client/src/app/domain/definitions/delegation-setting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Settings } from '../models/meetings/meeting';

export type DelegationSetting = Extract<
keyof Settings,
| `users_forbid_delegator_in_list_of_speakers`
| `users_forbid_delegator_as_submitter`
| `users_forbid_delegator_as_supporter`
>;

export const delegationSettings: { [key: string]: DelegationSetting } = {
ForbidLoS: `users_forbid_delegator_in_list_of_speakers`,
ForbidSubmitter: `users_forbid_delegator_as_submitter`,
ForbidSupporter: `users_forbid_delegator_as_supporter`
};
6 changes: 6 additions & 0 deletions client/src/app/domain/models/meetings/meeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ export class Settings {
public users_email_replyto!: string;
public users_email_subject!: string;
public users_email_body!: string;
public users_forbid_delegator_in_list_of_speakers!: string;
public users_forbid_delegator_as_submitter!: string;
public users_forbid_delegator_as_supporter!: string;

// Assignments
public assignments_export_title!: string;
Expand Down Expand Up @@ -409,6 +412,9 @@ export class Meeting extends BaseModel<Meeting> {
`users_email_subject`,
`users_email_body`,
`users_enable_vote_delegations`,
`users_forbid_delegator_in_list_of_speakers`,
`users_forbid_delegator_as_submitter`,
`users_forbid_delegator_as_supporter`,
`assignments_export_title`,
`assignments_export_preamble`,
`assignment_poll_ballot_paper_selection`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,16 @@
<span *ngIf="!canAddDueToPresence" translate>You have to be present to add yourself.</span>
</div>

<div class="add-self-buttons" *osPerms="permission.listOfSpeakersCanBeSpeaker">
<div
class="add-self-buttons"
*osPerms="
permission.listOfSpeakersCanBeSpeaker;
delegationSettingAlternative: [
'users_forbid_delegator_in_list_of_speakers',
permission.listOfSpeakersCanManage
]
"
>
<!-- Add me and remove me if OP has correct permission -->

<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,22 @@ export class MotionPermissionService {
public isAllowed(action: string, motion?: ViewMotion): boolean {
switch (action) {
case `create`: {
return this.operator.hasPerms(Permission.motionCanCreate);
return this.operator.hasPerms(
this.operator.isAllowedWithDelegation(`users_forbid_delegator_as_submitter`)
? Permission.motionCanCreate
: Permission.motionCanManage
);
}
case `support`: {
if (!motion || !motion.state) {
return false;
}
return (
this.operator.hasPerms(Permission.motionCanSupport) &&
this.operator.hasPerms(
this.operator.isAllowedWithDelegation(`users_forbid_delegator_as_supporter`)
? Permission.motionCanSupport
: Permission.motionCanManage
) &&
this.configMinSupporters > 0 &&
motion.state?.allow_support &&
(!motion.submitters ||
Expand All @@ -82,6 +90,11 @@ export class MotionPermissionService {
return false;
}
return (
this.operator.hasPerms(
this.operator.isAllowedWithDelegation(`users_forbid_delegator_as_supporter`)
? Permission.motionCanSupport
: Permission.motionCanManage
) &&
!!motion.state &&
motion.state.allow_support &&
motion.supporter_users &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,30 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([
key: `users_enable_vote_delegations`,
label: _(`Activate vote delegations`),
type: `boolean`
},
{
key: `users_forbid_delegator_in_list_of_speakers`,
label: _(`Restrict delegation principals from adding themselves to the list of speakers.`),
helpText: _(
`Enabling this will cause non-present participants, whose voting rights have been delegated, to be unable to add themselves to any list of speakers unless they have managing rights.`
),
type: `boolean`
},
{
key: `users_forbid_delegator_as_submitter`,
label: _(`Restrict delegation principals from creating motions.`),
helpText: _(
`Enabling this will cause non-present participants, whose voting rights have been delegated, to be unable to create motions unless they have managing rights.`
),
type: `boolean`
},
{
key: `users_forbid_delegator_as_supporter`,
label: _(`Restrict delegation principals from supporting motions.`),
helpText: _(
`Enabling this will cause non-present participants, whose voting rights have been delegated, to be unable to support motions unless they have managing rights.`
),
type: `boolean`
}
]
},
Expand Down
44 changes: 42 additions & 2 deletions client/src/app/site/services/operator.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, debounceTime, Observable, Subject } from 'rxjs';
import {
BehaviorSubject,
combineLatest,
debounceTime,
distinctUntilChanged,
Observable,
startWith,
Subject
} from 'rxjs';
import { DelegationSetting, delegationSettings } from 'src/app/domain/definitions/delegation-setting';
import { UserFieldsets } from 'src/app/domain/fieldsets/user';
import { Settings } from 'src/app/domain/models/meetings/meeting';
import { MeetingUserRepositoryService } from 'src/app/gateways/repositories/meeting_user';
import { UserRepositoryService } from 'src/app/gateways/repositories/users';
import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user';
import { ModelRequestBuilderService } from 'src/app/site/services/model-request-builder';
Expand All @@ -16,6 +27,7 @@ import { GroupControllerService } from '../pages/meetings/pages/participants';
import { ActiveMeetingService } from '../pages/meetings/services/active-meeting.service';
import { NoActiveMeetingError } from '../pages/meetings/services/active-meeting-id.service';
import { MeetingControllerService } from '../pages/meetings/services/meeting-controller.service';
import { MeetingSettingsService } from '../pages/meetings/services/meeting-settings.service';
import { ViewMeeting } from '../pages/meetings/view-models/view-meeting';
import { AuthService } from './auth.service';
import { AutoupdateService, ModelSubscription } from './autoupdate';
Expand Down Expand Up @@ -110,6 +122,10 @@ export class OperatorService {
return this._operatorUpdatedSubject;
}

public get delegationSettingsUpdated(): Observable<void> {
return this._delegationSettingsUpdatedSubject;
}

public get operatorShortNameObservable(): Observable<string | null> {
return this._operatorShortNameSubject;
}
Expand Down Expand Up @@ -176,6 +192,8 @@ export class OperatorService {
null
);

private readonly _delegationSettingsUpdatedSubject = new Subject<void>();

private readonly _userSubject = new BehaviorSubject<ViewUser | null>(null);
private readonly _operatorReadySubject = new BehaviorSubject<boolean>(false);

Expand Down Expand Up @@ -218,10 +236,12 @@ export class OperatorService {
private authService: AuthService,
private lifecycle: LifecycleService,
private userRepo: UserRepositoryService,
private meetingUserRepo: MeetingUserRepositoryService,
private groupRepo: GroupControllerService,
private autoupdateService: AutoupdateService,
private modelRequestBuilder: ModelRequestBuilderService,
private meetingRepo: MeetingControllerService
private meetingRepo: MeetingControllerService,
private meetingSettings: MeetingSettingsService
) {
this.setNotReady();
// General environment in which the operator moves
Expand Down Expand Up @@ -279,6 +299,11 @@ export class OperatorService {
this._operatorUpdatedSubject.next();
}
});
this.meetingUserRepo.getGeneralViewModelObservable().subscribe(user => {
if (user !== undefined && this.operatorId === user.user_id) {
this._operatorUpdatedSubject.next();
}
});
this.groupRepo.getGeneralViewModelObservable().subscribe(group => {
if (!this.activeMeetingId || !group) {
return;
Expand Down Expand Up @@ -318,6 +343,13 @@ export class OperatorService {
this._groupsLoadedDeferred.resolve();
}
});
combineLatest(
Object.values(delegationSettings).map(setting =>
this.meetingSettings.get(setting as keyof Settings).pipe(startWith(null))
)
)
.pipe(distinctUntilChanged())
.subscribe(() => this._delegationSettingsUpdatedSubject.next());
}

public isInMeeting(meetingId: Id): boolean {
Expand Down Expand Up @@ -537,6 +569,14 @@ export class OperatorService {
return checkPerms.some(permission => groups.some(group => group.hasPermission(permission)));
}

public isAllowedWithDelegation(...appliedSettings: DelegationSetting[]): boolean {
return (
this.user.isPresentInMeeting(this.activeMeetingId) ||
!this.user.getMeetingUser(this.activeMeetingId)?.vote_delegated_to_id ||
!appliedSettings.some(appliedSetting => this.meetingSettings.instant(appliedSetting as keyof Settings))
);
}

/**
* Checks, if the own OML is equals or higher than at least one of the given permissions.
*
Expand Down
54 changes: 54 additions & 0 deletions client/src/app/ui/directives/perms/perms.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable, Subject } from 'rxjs';
import { DelegationSetting } from 'src/app/domain/definitions/delegation-setting';
import { Permission } from 'src/app/domain/definitions/permission';
import { OperatorService } from 'src/app/site/services/operator.service';

Expand Down Expand Up @@ -29,10 +30,18 @@ export class BasePermsTestComponent<ComponentDataType extends object> {
<div id="or" *osPerms="permission; or: conditionals.or"></div>
<div id="and" *osPerms="permission; and: conditionals.and"></div>
<div id="complement" *osPerms="permission; complement: conditionals.complement"></div>
<div
id="delegation"
*osPerms="
permission;
delegationSettingAlternative: ['users_forbid_delegator_in_list_of_speakers', altPermission]
"
></div>
`
})
class TestComponent extends BasePermsTestComponent<TestConditionalType> {
public readonly permission = Permission.listOfSpeakersCanSee;
public readonly altPermission = Permission.listOfSpeakersCanManage;

public constructor() {
super({ and: true, or: true, complement: true });
Expand All @@ -44,17 +53,38 @@ class MockOperatorService {
return this._operatorUpdatedSubject;
}

public get delegationSettingsUpdated(): Observable<void> {
return this._delegationSettingsUpdatedSubject;
}

private _operatorUpdatedSubject = new Subject<void>();
private _delegationSettingsUpdatedSubject = new Subject<void>();
private _permList: Permission[] = [];
private _appliedSettings: DelegationSetting[] = [];
private _user_delegated = false;

public hasPerms(...checkPerms: Permission[]): boolean {
return checkPerms.some(perm => this._permList.includes(perm));
}

public isAllowedWithDelegation(...appliedSettings: DelegationSetting[]): boolean {
return !appliedSettings.some(setting => this._appliedSettings.includes(setting)) || !this._user_delegated;
}

public changeOperatorPermsForTest(newPermList: Permission[]): void {
this._permList = newPermList;
this._operatorUpdatedSubject.next();
}

public changeOperatorDelegationForTest(delegated: boolean): void {
this._user_delegated = delegated;
this._operatorUpdatedSubject.next();
}

public changeDelegatorSettingsForTest(appliedSettings: DelegationSetting[]): void {
this._appliedSettings = appliedSettings;
this._delegationSettingsUpdatedSubject.next();
}
}

describe(`PermsDirective`, () => {
Expand Down Expand Up @@ -117,6 +147,30 @@ describe(`PermsDirective`, () => {
expect(getElement(`#and`)).toBeTruthy();
});

it(`check if delegationSettingAlternative works`, async () => {
expect(getElement(`#delegation`)).toBeFalsy();
operatorService.changeOperatorPermsForTest([Permission.listOfSpeakersCanSee]);
update();
expect(getElement(`#delegation`)).toBeTruthy();
operatorService.changeOperatorDelegationForTest(true);
update();
expect(getElement(`#delegation`)).toBeTruthy();
operatorService.changeDelegatorSettingsForTest([`users_forbid_delegator_in_list_of_speakers`]);
update();
expect(getElement(`#delegation`)).toBeFalsy();
operatorService.changeDelegatorSettingsForTest([`users_forbid_delegator_as_submitter`]);
update();
expect(getElement(`#delegation`)).toBeTruthy();
operatorService.changeDelegatorSettingsForTest([`users_forbid_delegator_in_list_of_speakers`]);
operatorService.changeOperatorPermsForTest([Permission.listOfSpeakersCanManage]);
update();
expect(getElement(`#delegation`)).toBeTruthy();
operatorService.changeOperatorPermsForTest([Permission.listOfSpeakersCanSee]);
operatorService.changeOperatorDelegationForTest(false);
update();
expect(getElement(`#delegation`)).toBeTruthy();
});

it(`check if complement works`, async () => {
expect(getElement(`#complement`)).toBeTruthy();
operatorService.changeOperatorPermsForTest([Permission.listOfSpeakersCanSee]);
Expand Down
45 changes: 42 additions & 3 deletions client/src/app/ui/directives/perms/perms.directive.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Directive, Input } from '@angular/core';
import { Directive, Input, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { DelegationSetting } from 'src/app/domain/definitions/delegation-setting';
import { Permission } from 'src/app/domain/definitions/permission';

import { BasePermsDirective } from './base-perms.directive';

@Directive({
selector: `[osPerms]`
})
export class PermsDirective extends BasePermsDirective<Permission> {
export class PermsDirective extends BasePermsDirective<Permission> implements OnInit, OnDestroy {
/**
* The value defines the requires permissions as an array or a single permission.
*/
Expand All @@ -33,6 +35,15 @@ export class PermsDirective extends BasePermsDirective<Permission> {
this.setComplementCondition(value);
}

/**
* `*osPerms="...; delegationSettingAlternative:..."` turns into osPermsDelegationSettingAlternative during runtime.
*/
@Input()
public set osPermsDelegationSettingAlternative(delegationSettingAlternative: [DelegationSetting, Permission]) {
this._delegationSettingAlternative = delegationSettingAlternative;
this.updatePermission();
}

/**
* `*osPerms="...; and:..."` turns into osPermsAnd during runtime.
*/
Expand All @@ -41,11 +52,39 @@ export class PermsDirective extends BasePermsDirective<Permission> {
this.setAndCondition(value);
}

private _delegationSettingAlternative: [DelegationSetting, Permission] = null;
private _delegationSettingSubscription: Subscription;

public override ngOnInit(): void {
// observe groups of operator, so the directive can actively react to changes
this._delegationSettingSubscription = this.operator.delegationSettingsUpdated.subscribe(() => {
this.updatePermission();
});
super.ngOnInit();
}

public override ngOnDestroy(): void {
if (this._delegationSettingSubscription) {
this._delegationSettingSubscription.unsubscribe();
this._delegationSettingSubscription = null;
}
super.ngOnDestroy();
}

/**
* Compare the required permissions with the users permissions.
* Returns true if the users permissions fit.
*/
protected hasPermissions(): boolean {
return this.operator.hasPerms(...this.permissions);
console.log(`HAS PERM`, this.permissions);
let permissions = this.permissions;
if (
this._delegationSettingAlternative &&
!this.operator.isAllowedWithDelegation(this._delegationSettingAlternative[0])
) {
console.log(`->`, this._delegationSettingAlternative[1]);
permissions = [this._delegationSettingAlternative[1]];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log(`->`, this._delegationSettingAlternative[1]);

}
return this.operator.hasPerms(...permissions);
}
}
2 changes: 1 addition & 1 deletion client/src/meta
Loading