Merge pull request #4461 from tsiegleauq/permission-list-view

Add permissions to ListViews
This commit is contained in:
Sean 2019-04-06 21:33:44 +02:00 committed by GitHub
commit 2f330933a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 327 additions and 202 deletions

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; import { CanActivate, ActivatedRouteSnapshot, CanActivateChild, Router } from '@angular/router';
import { OperatorService } from './operator.service'; import { OperatorService } from './operator.service';
@ -11,9 +11,12 @@ import { OperatorService } from './operator.service';
}) })
export class AuthGuard implements CanActivate, CanActivateChild { export class AuthGuard implements CanActivate, CanActivateChild {
/** /**
* @param operator * Constructor
*
* @param router To navigate to a target URL
* @param operator Asking for the required permission
*/ */
public constructor(private operator: OperatorService) {} public constructor(private router: Router, private operator: OperatorService) {}
/** /**
* Checks of the operator has the required permission to see the state. * Checks of the operator has the required permission to see the state.
@ -22,10 +25,9 @@ export class AuthGuard implements CanActivate, CanActivateChild {
* `data: {basePerm: ['<perm1>', '<perm2>']}` to lock the access to users * `data: {basePerm: ['<perm1>', '<perm2>']}` to lock the access to users
* only with the given permission(s). * only with the given permission(s).
* *
* @param route required by `canActivate()` * @param route the route the user wants to navigate to
* @param state the state (URL) that the user want to access
*/ */
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { public canActivate(route: ActivatedRouteSnapshot): boolean {
const basePerm: string | string[] = route.data.basePerm; const basePerm: string | string[] = route.data.basePerm;
if (!basePerm) { if (!basePerm) {
@ -39,10 +41,19 @@ export class AuthGuard implements CanActivate, CanActivateChild {
/** /**
* Calls {@method canActivate}. Should have the same logic. * Calls {@method canActivate}. Should have the same logic.
* @param route *
* @param state * @param route the route the user wants to navigate to
*/ */
public canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { public canActivateChild(route: ActivatedRouteSnapshot): boolean {
return this.canActivate(route, state); if (this.canActivate(route)) {
return true;
} else {
this.router.navigate(['/error'], {
queryParams: {
error: 'Authentication Error',
msg: route.data.basePerm
}
});
}
} }
} }

View File

@ -10,12 +10,17 @@ import { WatchSortingTreeGuard } from 'app/shared/utils/watch-sorting-tree.guard
const routes: Routes = [ const routes: Routes = [
{ path: '', component: AgendaListComponent, pathMatch: 'full' }, { path: '', component: AgendaListComponent, pathMatch: 'full' },
{ path: 'import', component: AgendaImportListComponent }, { path: 'import', component: AgendaImportListComponent, data: { basePerm: 'agenda.can_manage' } },
{ path: 'topics/new', component: TopicDetailComponent }, { path: 'topics/new', component: TopicDetailComponent, data: { basePerm: 'agenda.can_manage' } },
{ path: 'sort-agenda', component: AgendaSortComponent, canDeactivate: [WatchSortingTreeGuard] }, {
{ path: 'speakers', component: ListOfSpeakersComponent }, path: 'sort-agenda',
{ path: 'topics/:id', component: TopicDetailComponent }, component: AgendaSortComponent,
{ path: ':id/speakers', component: ListOfSpeakersComponent } canDeactivate: [WatchSortingTreeGuard],
data: { basePerm: 'agenda.can_manage' }
},
{ path: 'speakers', component: ListOfSpeakersComponent, data: { basePerm: 'agenda.can_see' } },
{ path: 'topics/:id', component: TopicDetailComponent, data: { basePerm: 'agenda.can_see' } },
{ path: ':id/speakers', component: ListOfSpeakersComponent, data: { basePerm: 'agenda.can_see' } }
]; ];
@NgModule({ @NgModule({

View File

@ -12,6 +12,7 @@
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span> <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div> </div>
</os-head-bar> </os-head-bar>
<mat-drawer-container class="on-transition-fade"> <mat-drawer-container class="on-transition-fade">
<os-sort-filter-bar <os-sort-filter-bar
[filterCount]="filteredCount" [filterCount]="filteredCount"

View File

@ -5,12 +5,14 @@ import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-poli
import { StartComponent } from './components/start/start.component'; import { StartComponent } from './components/start/start.component';
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component'; import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
import { SearchComponent } from './components/search/search.component'; import { SearchComponent } from './components/search/search.component';
import { ErrorComponent } from './components/error/error.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: StartComponent, component: StartComponent,
pathMatch: 'full' pathMatch: 'full',
data: { basePerm: 'core.can_see_frontpage' }
}, },
{ {
path: 'legalnotice', path: 'legalnotice',
@ -23,6 +25,10 @@ const routes: Routes = [
{ {
path: 'search', path: 'search',
component: SearchComponent component: SearchComponent
},
{
path: 'error',
component: ErrorComponent
} }
]; ];

View File

@ -0,0 +1,9 @@
<os-head-bar>
<div class="title-slot">
<h2 translate>Error</h2>
</div>
</os-head-bar>
<mat-card class="os-card on-transition-fade">
<h1 translate>You do not have the required permission to see that page!</h1>
</mat-card>

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorComponent } from './error.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('ErrorComponent', () => {
let component: ErrorComponent;
let fixture: ComponentFixture<ErrorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ErrorComponent],
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ErrorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
/**
* A component to show error states
*/
@Component({
selector: 'os-error',
templateUrl: './error.component.html',
styleUrls: ['./error.component.scss']
})
export class ErrorComponent implements OnInit {
/**
* Constructor
*
* @param route get paramters
*/
public constructor(private route: ActivatedRoute) {}
/**
* Show the required debug output in the log
*/
public ngOnInit(): void {
this.route.queryParams.subscribe(params => {
if (params && params.error) {
// print the error and the error message in terminal for debug purposes.
// Will make it easier tell where user errors are
console.error(`${params.error}! Required: "${params.msg}"`);
}
});
}
}

View File

@ -7,6 +7,6 @@
<mat-card class="os-card"> <mat-card class="os-card">
<div class="app-content" translate> <div class="app-content" translate>
<h1>{{ welcomeTitle | translate }}</h1> <h1>{{ welcomeTitle | translate }}</h1>
<div [innerHTML]="(welcomeText) | translate"></div> <div [innerHTML]="welcomeText | translate"></div>
</div> </div>
</mat-card> </mat-card>

View File

@ -8,9 +8,17 @@ import { StartComponent } from './components/start/start.component';
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component'; import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
import { SearchComponent } from './components/search/search.component'; import { SearchComponent } from './components/search/search.component';
import { CountUsersComponent } from './components/count-users/count-users.component'; import { CountUsersComponent } from './components/count-users/count-users.component';
import { ErrorComponent } from './components/error/error.component';
@NgModule({ @NgModule({
imports: [CommonModule, CommonRoutingModule, SharedModule], imports: [CommonModule, CommonRoutingModule, SharedModule],
declarations: [PrivacyPolicyComponent, StartComponent, LegalNoticeComponent, SearchComponent, CountUsersComponent] declarations: [
PrivacyPolicyComponent,
StartComponent,
LegalNoticeComponent,
SearchComponent,
CountUsersComponent,
ErrorComponent
]
}) })
export class OsCommonModule {} export class OsCommonModule {}

View File

@ -17,6 +17,7 @@
<mat-icon matSuffix>search</mat-icon> <mat-icon matSuffix>search</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-table class="os-headed-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-headed-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Timestamp --> <!-- Timestamp -->
<ng-container matColumnDef="time"> <ng-container matColumnDef="time">

View File

@ -1,5 +1,5 @@
<os-head-bar <os-head-bar
[mainButton]="canEdit" [mainButton]="canUploadFiles"
[editMode]="editFile" [editMode]="editFile"
[multiSelectMode]="isMultiSelect" [multiSelectMode]="isMultiSelect"
(mainEvent)="onMainEvent()" (mainEvent)="onMainEvent()"
@ -38,7 +38,7 @@
</div> </div>
<!-- Menu --> <!-- Menu -->
<div class="menu-slot" *ngIf="canEdit"> <div class="menu-slot" *osPerms="'mediafiles.can_manage'">
<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu"> <button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
@ -142,24 +142,30 @@
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
</mat-drawer-container>
<mat-menu #singleFileMenu="matMenu"> <!-- Template for the managing buttons -->
<ng-template #manageButton let-file="file" let-action="action">
<button mat-menu-item (click)="onManageButton($event, file, action)">
<mat-icon color="accent"> {{ isUsedAs(file, action) ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
<span>{{ getNameOfAction(action) }}</span>
</button>
</ng-template>
<!-- Menu for single files in the list -->
<mat-menu #singleFileMenu="matMenu">
<ng-template matMenuContent let-file="file"> <ng-template matMenuContent let-file="file">
<!-- Exclusive for images --> <!-- Exclusive for images -->
<div *ngIf="file.isImage()"> <div *ngIf="file.isImage()">
<div *ngFor="let action of logoActions"> <div *ngFor="let action of logoActions">
<ng-container <ng-container *ngTemplateOutlet="manageButton; context: { file: file, action: action }"></ng-container>
*ngTemplateOutlet="manageButton; context: { file: file, action: action }"
></ng-container>
</div> </div>
</div> </div>
<!-- Exclusive for fonts --> <!-- Exclusive for fonts -->
<div *ngIf="file.isFont()"> <div *ngIf="file.isFont()">
<div *ngFor="let action of fontActions"> <div *ngFor="let action of fontActions">
<ng-container <ng-container *ngTemplateOutlet="manageButton; context: { file: file, action: action }"></ng-container>
*ngTemplateOutlet="manageButton; context: { file: file, action: action }"
></ng-container>
</div> </div>
</div> </div>
@ -174,18 +180,10 @@
<span translate>Delete</span> <span translate>Delete</span>
</button> </button>
</ng-template> </ng-template>
</mat-menu> </mat-menu>
<!-- Template for the managing buttons --> <!-- Menu for Mediafiles -->
<ng-template #manageButton let-file="file" let-action="action"> <mat-menu #mediafilesMenu="matMenu">
<button mat-menu-item (click)="onManageButton($event, file, action)">
<mat-icon color="accent"> {{ isUsedAs(file, action) ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
<span>{{ getNameOfAction(action) }}</span>
</button>
</ng-template>
<!-- Menu for Mediafiles -->
<mat-menu #mediafilesMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()"> <button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
@ -208,5 +206,4 @@
<span translate>Delete</span> <span translate>Delete</span>
</button> </button>
</div> </div>
</mat-menu> </mat-menu>
</mat-drawer-container>

View File

@ -60,6 +60,13 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
/** /**
* @returns true if the user can manage media files * @returns true if the user can manage media files
*/ */
public get canUploadFiles(): boolean {
return this.operator.hasPerms('mediafiles.can_see') && this.operator.hasPerms('mediafiles.can_upload');
}
/**
* @return true if the user can manage media files
*/
public get canEdit(): boolean { public get canEdit(): boolean {
return this.operator.hasPerms('mediafiles.can_manage'); return this.operator.hasPerms('mediafiles.can_manage');
} }

View File

@ -5,7 +5,7 @@ import { MediaUploadComponent } from './components/media-upload/media-upload.com
const routes: Routes = [ const routes: Routes = [
{ path: '', component: MediafileListComponent, pathMatch: 'full' }, { path: '', component: MediafileListComponent, pathMatch: 'full' },
{ path: 'upload', component: MediaUploadComponent } { path: 'upload', component: MediaUploadComponent, data: { basePerm: 'mediafiles.can_upload' } }
]; ];
@NgModule({ @NgModule({

View File

@ -704,7 +704,12 @@
listname="{{ 'Attachments' | translate }}" listname="{{ 'Attachments' | translate }}"
[InputListValues]="mediafilesObserver" [InputListValues]="mediafilesObserver"
></os-search-value-selector> ></os-search-value-selector>
<button type="button" mat-icon-button (click)="onUploadAttachmentsButton(uploadDialog)"> <button
type="button"
mat-icon-button
(click)="onUploadAttachmentsButton(uploadDialog)"
*osPerms="'mediafiles.can_upload'"
>
<mat-icon>cloud_upload</mat-icon> <mat-icon>cloud_upload</mat-icon>
</button> </button>
</div> </div>

View File

@ -9,40 +9,49 @@ const routes: Routes = [
}, },
{ {
path: 'import', path: 'import',
loadChildren: './modules/motion-import/motion-import.module#MotionImportModule' loadChildren: './modules/motion-import/motion-import.module#MotionImportModule',
data: { basePerm: 'motions.can_manage' }
}, },
{ {
path: 'statute-paragraphs', path: 'statute-paragraphs',
loadChildren: './modules/statute-paragraph/statute-paragraph.module#StatuteParagraphModule' loadChildren: './modules/statute-paragraph/statute-paragraph.module#StatuteParagraphModule',
data: { basePerm: 'motions.can_manage' }
}, },
{ {
path: 'comment-section', path: 'comment-section',
loadChildren: './modules/motion-comment-section/motion-comment-section.module#MotionCommentSectionModule' loadChildren: './modules/motion-comment-section/motion-comment-section.module#MotionCommentSectionModule',
data: { basePerm: 'motions.can_manage' }
}, },
{ {
path: 'call-list', path: 'call-list',
loadChildren: './modules/call-list/call-list.module#CallListModule' loadChildren: './modules/call-list/call-list.module#CallListModule',
data: { basePerm: 'motions.can_manage' }
}, },
{ {
path: 'category', path: 'category',
loadChildren: './modules/category/category.module#CategoryModule' loadChildren: './modules/category/category.module#CategoryModule',
data: { basePerm: 'motions.can_see' }
}, },
{ {
path: 'blocks', path: 'blocks',
loadChildren: './modules/motion-block/motion-block.module#MotionBlockModule' loadChildren: './modules/motion-block/motion-block.module#MotionBlockModule',
data: { basePerm: 'motions.can_manage' }
}, },
{ {
path: 'workflow', path: 'workflow',
loadChildren: './modules/motion-workflow/motion-workflow.module#MotionWorkflowModule' loadChildren: './modules/motion-workflow/motion-workflow.module#MotionWorkflowModule',
data: { basePerm: 'motions.can_manage' }
}, },
{ {
path: 'new', path: 'new',
loadChildren: './modules/motion-detail/motion-detail.module#MotionDetailModule' loadChildren: './modules/motion-detail/motion-detail.module#MotionDetailModule',
data: { basePerm: 'motions.can_create' }
}, },
{ {
path: ':id', path: ':id',
loadChildren: './modules/motion-detail/motion-detail.module#MotionDetailModule', loadChildren: './modules/motion-detail/motion-detail.module#MotionDetailModule',
runGuardsAndResolvers: 'paramsChange' runGuardsAndResolvers: 'paramsChange',
data: { basePerm: 'motions.can_see' }
} }
]; ];

View File

@ -6,9 +6,7 @@
</os-head-bar> </os-head-bar>
<mat-card *ngIf="!projectorToCreate && projectors && projectors.length > 1"> <mat-card *ngIf="!projectorToCreate && projectors && projectors.length > 1">
<span translate> <span translate> Reference projector for current list of speakers: </span>&nbsp;
Reference projector for current list of speakers: </span
>&nbsp;
<mat-form-field> <mat-form-field>
<mat-select <mat-select
[disabled]="!!editId" [disabled]="!!editId"

View File

@ -11,7 +11,8 @@ const routes: Routes = [
}, },
{ {
path: 'detail/:id', path: 'detail/:id',
component: ProjectorDetailComponent component: ProjectorDetailComponent,
data: { basePerm: 'core.can_see_projector' }
} }
]; ];

View File

@ -20,39 +20,48 @@ const routes: Routes = [
}, },
{ {
path: 'agenda', path: 'agenda',
loadChildren: './agenda/agenda.module#AgendaModule' loadChildren: './agenda/agenda.module#AgendaModule',
data: { basePerm: 'agenda.can_see' }
}, },
{ {
path: 'assignments', path: 'assignments',
loadChildren: './assignments/assignments.module#AssignmentsModule' loadChildren: './assignments/assignments.module#AssignmentsModule',
data: { basePerm: 'assignment.can_see' }
}, },
{ {
path: 'mediafiles', path: 'mediafiles',
loadChildren: './mediafiles/mediafiles.module#MediafilesModule' loadChildren: './mediafiles/mediafiles.module#MediafilesModule',
data: { basePerm: 'mediafiles.can_see' }
}, },
{ {
path: 'motions', path: 'motions',
loadChildren: './motions/motions.module#MotionsModule' loadChildren: './motions/motions.module#MotionsModule',
data: { basePerm: 'motions.can_see' }
}, },
{ {
path: 'settings', path: 'settings',
loadChildren: './config/config.module#ConfigModule' loadChildren: './config/config.module#ConfigModule',
data: { basePerm: 'core.can_manage_config' }
}, },
{ {
path: 'users', path: 'users',
loadChildren: './users/users.module#UsersModule' loadChildren: './users/users.module#UsersModule',
data: { basePerm: 'users.can_see_name' }
}, },
{ {
path: 'tags', path: 'tags',
loadChildren: './tags/tag.module#TagModule' loadChildren: './tags/tag.module#TagModule',
data: { basePerm: 'core.can_manage_tags' }
}, },
{ {
path: 'history', path: 'history',
loadChildren: './history/history.module#HistoryModule' loadChildren: './history/history.module#HistoryModule',
data: { basePerm: 'core.can_see_history' }
}, },
{ {
path: 'projectors', path: 'projectors',
loadChildren: './projector/projector.module#ProjectorModule' loadChildren: './projector/projector.module#ProjectorModule',
data: { basePerm: 'core.can_see_projector' }
} }
], ],
canActivateChild: [AuthGuard] canActivateChild: [AuthGuard]

View File

@ -105,8 +105,9 @@
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
</mat-drawer-container>
<mat-menu #userMenu="matMenu"> <mat-menu #userMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">
<button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()"> <button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
@ -212,8 +213,7 @@
</div> </div>
</div> </div>
</div> </div>
</mat-menu> </mat-menu>
</mat-drawer-container>
<ng-template #userInfoDialog> <ng-template #userInfoDialog>
<h1 mat-dialog-title> <h1 mat-dialog-title>

View File

@ -16,39 +16,39 @@ const routes: Routes = [
}, },
{ {
path: 'password', path: 'password',
component: PasswordComponent component: PasswordComponent,
data: { basePerm: 'users.can_change_password' }
}, },
{ {
path: 'password/:id', path: 'password/:id',
component: PasswordComponent component: PasswordComponent,
data: { basePerm: 'can_manage' }
}, },
{ {
path: 'new', path: 'new',
component: UserDetailComponent component: UserDetailComponent,
data: { basePerm: 'users.can_manage' }
}, },
{ {
path: 'import', path: 'import',
component: UserImportListComponent component: UserImportListComponent,
data: { basePerm: 'users.can_manage' }
}, },
{ {
path: 'presence', path: 'presence',
component: PresenceDetailComponent component: PresenceDetailComponent,
// FIXME: CRITICAL: restricted to basePerm: 'users.can_manage' and config 'users_enable_presence_view' // TODO: 'users_enable_presence_view' missing in permissions
data: { basePerm: 'users.can_manage' }
}, },
{ {
path: 'groups', path: 'groups',
component: GroupListComponent component: GroupListComponent,
/** data: { basePerm: 'users.can_manage' }
* FIXME: CRITICAL:
* Refreshing the page, even while having the required permission, will navigate you back to "/"
* Makes developing protected areas impossible.
* Has the be (temporarily) removed if this page should be edited.
*/
// data: { basePerm: 'users.can_manage' }
}, },
{ {
path: ':id', path: ':id',
component: UserDetailComponent component: UserDetailComponent,
data: { basePerm: 'users.can_see_name' }
} }
]; ];

View File

@ -83,7 +83,7 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
""" """
# Parse data. # Parse data.
if await async_has_perm(user_id, "motions.can_see"): if await async_has_perm(user_id, "motions.can_see"):
has_manage_perms = await async_has_perm(user_id, "motion.can_manage") has_manage_perms = await async_has_perm(user_id, "motions.can_manage")
data = [] data = []
for full in full_data: for full in full_data:
if not full["internal"] or has_manage_perms: if not full["internal"] or has_manage_perms: