[ACS-10409] Fix permission checking logic in library navigation (#4824)

* [ACS-10409] Fix permission checking logic in library navigation

* [ACS-10409] cr fix
This commit is contained in:
Mykyta Maliarchuk
2025-10-14 09:50:49 +02:00
committed by GitHub
parent 55a899b177
commit e590839e69
7 changed files with 135 additions and 37 deletions
@@ -36,6 +36,7 @@ import { LibraryEffects } from '../../store/effects';
import { NodeEntry } from '@alfresco/js-api'; import { NodeEntry } from '@alfresco/js-api';
import { getTitleElementText } from '../../testing/test-utils'; import { getTitleElementText } from '../../testing/test-utils';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { SiteEntry } from '@alfresco/js-api/typings';
describe('FavoriteLibrariesComponent', () => { describe('FavoriteLibrariesComponent', () => {
let fixture: ComponentFixture<FavoriteLibrariesComponent>; let fixture: ComponentFixture<FavoriteLibrariesComponent>;
@@ -126,7 +127,13 @@ describe('FavoriteLibrariesComponent', () => {
it('does not navigate when id is not passed', () => { it('does not navigate when id is not passed', () => {
spyOn(router, 'navigate').and.stub(); spyOn(router, 'navigate').and.stub();
component.navigateTo({ entry: { guid: 'guid' } } as any); component.navigateTo({
entry: {
guid: 'test-guid',
visibility: 'PUBLIC',
role: 'SiteConsumer'
}
} as SiteEntry);
expect(router.navigate).toHaveBeenCalledWith(['favorite/libraries', 'libraryId']); expect(router.navigate).toHaveBeenCalledWith(['favorite/libraries', 'libraryId']);
}); });
@@ -104,7 +104,7 @@ export class FavoriteLibrariesComponent extends PageComponent implements OnInit
navigateTo(node: SiteEntry) { navigateTo(node: SiteEntry) {
if (node?.entry?.guid) { if (node?.entry?.guid) {
this.store.dispatch(new NavigateLibraryAction(node.entry.guid, 'favorite/libraries')); this.store.dispatch(new NavigateLibraryAction(node.entry, 'favorite/libraries'));
} }
} }
@@ -89,7 +89,7 @@ export class LibrariesComponent extends PageComponent implements OnInit {
navigateTo(node: SiteEntry) { navigateTo(node: SiteEntry) {
if (node?.entry?.guid) { if (node?.entry?.guid) {
this.store.dispatch(new NavigateLibraryAction(node.entry.guid)); this.store.dispatch(new NavigateLibraryAction(node.entry));
} }
} }
@@ -168,7 +168,7 @@ export class SearchLibrariesResultsComponent extends PageComponent implements On
navigateTo(node: SiteEntry) { navigateTo(node: SiteEntry) {
if (node?.entry?.guid) { if (node?.entry?.guid) {
this.store.dispatch(new NavigateLibraryAction(node.entry.guid)); this.store.dispatch(new NavigateLibraryAction(node.entry));
} }
} }
@@ -32,7 +32,7 @@ import { NotificationService } from '@alfresco/adf-core';
import { provideEffects } from '@ngrx/effects'; import { provideEffects } from '@ngrx/effects';
import { LibraryEffects } from './library.effects'; import { LibraryEffects } from './library.effects';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
import { NodeEntry } from '@alfresco/js-api'; import { NodeEntry, Site } from '@alfresco/js-api';
describe('LibraryEffects', () => { describe('LibraryEffects', () => {
let store: Store<AppStore>; let store: Store<AppStore>;
@@ -48,31 +48,106 @@ describe('LibraryEffects', () => {
describe('navigateLibrary$', () => { describe('navigateLibrary$', () => {
let notificationService: NotificationService; let notificationService: NotificationService;
let node$: Subject<NodeEntry>; let node$: Subject<NodeEntry>;
let admin$: Subject<boolean>;
let site: Site;
beforeEach(() => { beforeEach(() => {
node$ = new Subject<NodeEntry>(); node$ = new Subject<NodeEntry>();
admin$ = new Subject<boolean>();
site = { guid: 'site-guid', visibility: 'PUBLIC', role: 'SiteConsumer', id: 'site-id', title: 'Title' };
spyOn(TestBed.inject(ContentApiService), 'getNode').and.returnValue(node$); spyOn(TestBed.inject(ContentApiService), 'getNode').and.returnValue(node$);
notificationService = TestBed.inject(NotificationService); notificationService = TestBed.inject(NotificationService);
spyOn(notificationService, 'showError'); spyOn(notificationService, 'showError');
spyOn(store, 'dispatch').and.callThrough();
spyOn(store, 'select').and.returnValue(admin$);
}); });
it('should display library no permission warning if user does not have permission', () => { it('should display library no permission warning if user does not have permission', () => {
spyOn(notificationService, 'showWarning'); spyOn(notificationService, 'showWarning');
store.dispatch(new NavigateLibraryAction('libraryId')); store.dispatch(new NavigateLibraryAction(site));
admin$.next(false);
node$.error(new HttpErrorResponse({ status: 403 })); node$.error(new HttpErrorResponse({ status: 403 }));
expect(notificationService.showWarning).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING'); expect(notificationService.showWarning).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
}); });
it('should display library not found error if library does not exist', () => { it('should display library not found error if library does not exist', () => {
store.dispatch(new NavigateLibraryAction('libraryId')); store.dispatch(new NavigateLibraryAction(site));
admin$.next(false);
node$.error(new HttpErrorResponse({ status: 404 })); node$.error(new HttpErrorResponse({ status: 404 }));
expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NOT_FOUND'); expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NOT_FOUND');
}); });
it('should display generic library loading error if there is different problem than missing permissions or absence of library', () => { it('should display generic library loading error if there is different problem than missing permissions or absence of library', () => {
store.dispatch(new NavigateLibraryAction('libraryId')); store.dispatch(new NavigateLibraryAction(site));
admin$.next(false);
node$.error(new HttpErrorResponse({ status: 500 })); node$.error(new HttpErrorResponse({ status: 500 }));
expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR'); expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR');
}); });
it('should show error when non-admin user without site role tries to access private site', () => {
store.dispatch(new NavigateLibraryAction({ ...site, visibility: 'PRIVATE', role: null }));
admin$.next(false);
expect(store.dispatch).not.toHaveBeenCalledWith(
jasmine.objectContaining({
type: 'NAVIGATE_ROUTE'
})
);
expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
});
it('should allow admin user to navigate to private site without role', () => {
store.dispatch(new NavigateLibraryAction({ ...site, visibility: 'PRIVATE', role: null }));
admin$.next(true);
node$.next({ entry: { id: 'private-doc-lib-id' } } as NodeEntry);
expect(store.dispatch).toHaveBeenCalledWith(
jasmine.objectContaining({
type: 'NAVIGATE_ROUTE',
payload: ['libraries', 'private-doc-lib-id']
})
);
expect(notificationService.showError).not.toHaveBeenCalled();
});
it('should allow navigation to public site for non-admin user without a role', () => {
store.dispatch(new NavigateLibraryAction({ ...site, role: null }));
admin$.next(false);
node$.next({ entry: { id: 'public-doc-lib-id' } } as NodeEntry);
expect(store.dispatch).toHaveBeenCalledWith(
jasmine.objectContaining({
type: 'NAVIGATE_ROUTE',
payload: ['libraries', 'public-doc-lib-id']
})
);
expect(notificationService.showError).not.toHaveBeenCalled();
});
it('should allow navigation to private site for non-admin user with a role', () => {
store.dispatch(new NavigateLibraryAction({ ...site, visibility: 'PRIVATE' }));
admin$.next(false);
node$.next({ entry: { id: 'role-doc-lib-id' } } as NodeEntry);
expect(store.dispatch).toHaveBeenCalledWith(
jasmine.objectContaining({
type: 'NAVIGATE_ROUTE',
payload: ['libraries', 'role-doc-lib-id']
})
);
});
it('should use custom route if provided', () => {
store.dispatch(new NavigateLibraryAction(site, 'custom-route'));
admin$.next(false);
node$.next({ entry: { id: 'doc-lib-id' } } as NodeEntry);
expect(store.dispatch).toHaveBeenCalledWith(
jasmine.objectContaining({
type: 'NAVIGATE_ROUTE',
payload: ['custom-route', 'doc-lib-id']
})
);
});
}); });
}); });
@@ -31,12 +31,13 @@ import {
LibraryActionTypes, LibraryActionTypes,
NavigateLibraryAction, NavigateLibraryAction,
NavigateRouteAction, NavigateRouteAction,
UpdateLibraryAction UpdateLibraryAction,
isAdmin
} from '@alfresco/aca-shared/store'; } from '@alfresco/aca-shared/store';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { map, mergeMap, take } from 'rxjs/operators'; import { map, mergeMap, take, tap } from 'rxjs/operators';
import { ContentApiService } from '@alfresco/aca-shared'; import { ContentApiService } from '@alfresco/aca-shared';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
import { NotificationService } from '@alfresco/adf-core'; import { NotificationService } from '@alfresco/adf-core';
@@ -99,27 +100,46 @@ export class LibraryEffects {
this.actions$.pipe( this.actions$.pipe(
ofType<CreateLibraryAction>(LibraryActionTypes.Create), ofType<CreateLibraryAction>(LibraryActionTypes.Create),
mergeMap(() => this.content.createLibrary()), mergeMap(() => this.content.createLibrary()),
map((libraryId) => new NavigateLibraryAction(libraryId)) tap((libraryId) => this.navigateToLibraryById(libraryId))
), ),
{ dispatch: true } { dispatch: false }
); );
navigateLibrary$ = createEffect( navigateLibrary$ = createEffect(
() => () =>
this.actions$.pipe( this.actions$.pipe(
ofType<NavigateLibraryAction>(LibraryActionTypes.Navigate), ofType<NavigateLibraryAction>(LibraryActionTypes.Navigate),
map((action) => { tap((action) => {
const libraryId = action.payload; const payload = action.payload;
if (libraryId) { if (payload && 'guid' in payload) {
this.store
.select(isAdmin)
.pipe(take(1))
.subscribe((isUserAdmin) => {
if (!isUserAdmin && payload.visibility !== 'PUBLIC' && !payload.role) {
this.notificationService.showError('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
} else {
this.navigateToLibraryById(payload.guid, action.route);
}
});
}
})
),
{ dispatch: false }
);
private navigateToLibraryById(libraryId: string, route = 'libraries'): void {
this.contentApi this.contentApi
.getNode(libraryId, { relativePath: '/documentLibrary' }) .getNode(libraryId, { relativePath: '/documentLibrary' })
.pipe(map((node) => node.entry.id)) .pipe(
.subscribe( map((node) => node.entry.id),
(id) => { take(1)
const route = action.route ? action.route : 'libraries'; )
.subscribe({
next: (id) => {
this.store.dispatch(new NavigateRouteAction([route, id])); this.store.dispatch(new NavigateRouteAction([route, id]));
}, },
(error: HttpErrorResponse) => { error: (error: HttpErrorResponse) => {
switch (error.status) { switch (error.status) {
case 403: case 403:
this.notificationService.showWarning('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING'); this.notificationService.showWarning('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
@@ -131,12 +151,8 @@ export class LibraryEffects {
this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR'); this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR');
} }
} }
); });
} }
})
),
{ dispatch: false }
);
updateLibrary$ = createEffect( updateLibrary$ = createEffect(
() => () =>
@@ -23,7 +23,7 @@
*/ */
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { SiteBodyCreate } from '@alfresco/js-api'; import { Site, SiteBodyCreate } from '@alfresco/js-api';
import { ModalConfiguration } from '../models/modal-configuration'; import { ModalConfiguration } from '../models/modal-configuration';
export enum LibraryActionTypes { export enum LibraryActionTypes {
@@ -48,7 +48,7 @@ export class NavigateLibraryAction implements Action {
readonly type = LibraryActionTypes.Navigate; readonly type = LibraryActionTypes.Navigate;
constructor( constructor(
public payload?: string, public payload?: Site,
public route?: string public route?: string
) {} ) {}
} }