[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 { getTitleElementText } from '../../testing/test-utils';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { SiteEntry } from '@alfresco/js-api/typings';
describe('FavoriteLibrariesComponent', () => {
let fixture: ComponentFixture<FavoriteLibrariesComponent>;
@@ -126,7 +127,13 @@ describe('FavoriteLibrariesComponent', () => {
it('does not navigate when id is not passed', () => {
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']);
});
@@ -104,7 +104,7 @@ export class FavoriteLibrariesComponent extends PageComponent implements OnInit
navigateTo(node: SiteEntry) {
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) {
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) {
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 { LibraryEffects } from './library.effects';
import { AppTestingModule } from '../../testing/app-testing.module';
import { NodeEntry } from '@alfresco/js-api';
import { NodeEntry, Site } from '@alfresco/js-api';
describe('LibraryEffects', () => {
let store: Store<AppStore>;
@@ -48,31 +48,106 @@ describe('LibraryEffects', () => {
describe('navigateLibrary$', () => {
let notificationService: NotificationService;
let node$: Subject<NodeEntry>;
let admin$: Subject<boolean>;
let site: Site;
beforeEach(() => {
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$);
notificationService = TestBed.inject(NotificationService);
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', () => {
spyOn(notificationService, 'showWarning');
store.dispatch(new NavigateLibraryAction('libraryId'));
store.dispatch(new NavigateLibraryAction(site));
admin$.next(false);
node$.error(new HttpErrorResponse({ status: 403 }));
expect(notificationService.showWarning).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
});
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 }));
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', () => {
store.dispatch(new NavigateLibraryAction('libraryId'));
store.dispatch(new NavigateLibraryAction(site));
admin$.next(false);
node$.error(new HttpErrorResponse({ status: 500 }));
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,
NavigateLibraryAction,
NavigateRouteAction,
UpdateLibraryAction
UpdateLibraryAction,
isAdmin
} from '@alfresco/aca-shared/store';
import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
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 { ContentManagementService } from '../../services/content-management.service';
import { NotificationService } from '@alfresco/adf-core';
@@ -99,45 +100,60 @@ export class LibraryEffects {
this.actions$.pipe(
ofType<CreateLibraryAction>(LibraryActionTypes.Create),
mergeMap(() => this.content.createLibrary()),
map((libraryId) => new NavigateLibraryAction(libraryId))
tap((libraryId) => this.navigateToLibraryById(libraryId))
),
{ dispatch: true }
{ dispatch: false }
);
navigateLibrary$ = createEffect(
() =>
this.actions$.pipe(
ofType<NavigateLibraryAction>(LibraryActionTypes.Navigate),
map((action) => {
const libraryId = action.payload;
if (libraryId) {
this.contentApi
.getNode(libraryId, { relativePath: '/documentLibrary' })
.pipe(map((node) => node.entry.id))
.subscribe(
(id) => {
const route = action.route ? action.route : 'libraries';
this.store.dispatch(new NavigateRouteAction([route, id]));
},
(error: HttpErrorResponse) => {
switch (error.status) {
case 403:
this.notificationService.showWarning('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
break;
case 404:
this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NOT_FOUND');
break;
default:
this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR');
}
tap((action) => {
const payload = action.payload;
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
.getNode(libraryId, { relativePath: '/documentLibrary' })
.pipe(
map((node) => node.entry.id),
take(1)
)
.subscribe({
next: (id) => {
this.store.dispatch(new NavigateRouteAction([route, id]));
},
error: (error: HttpErrorResponse) => {
switch (error.status) {
case 403:
this.notificationService.showWarning('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
break;
case 404:
this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NOT_FOUND');
break;
default:
this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR');
}
}
});
}
updateLibrary$ = createEffect(
() =>
this.actions$.pipe(
@@ -23,7 +23,7 @@
*/
import { Action } from '@ngrx/store';
import { SiteBodyCreate } from '@alfresco/js-api';
import { Site, SiteBodyCreate } from '@alfresco/js-api';
import { ModalConfiguration } from '../models/modal-configuration';
export enum LibraryActionTypes {
@@ -48,7 +48,7 @@ export class NavigateLibraryAction implements Action {
readonly type = LibraryActionTypes.Navigate;
constructor(
public payload?: string,
public payload?: Site,
public route?: string
) {}
}