diff --git a/.github/actions/e2e/action.yml b/.github/actions/e2e/action.yml index 8f3c3f44f95..116bb8c9c51 100644 --- a/.github/actions/e2e/action.yml +++ b/.github/actions/e2e/action.yml @@ -153,7 +153,7 @@ runs: echo $PROXY_HOST_BPM echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV - - name: run test + - name: run test id: e2e_run if: ${{ steps.determine-affected.outputs.isAffected == 'true' }} env: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index df76626c4df..f2bb3312794 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -392,15 +392,6 @@ jobs: check-cs-env: "true" check-ps-cloud-env: "true" deps: "testing" - - description: "Process Cloud: People" - test-id: "process-services-cloud" - folder: "process-services-cloud/people" - provider: "ALL" - auth: "OAUTH" - apa-proxy: true - check-cs-env: "true" - check-ps-cloud-env: "true" - deps: "testing" - description: "Process Cloud: Process" test-id: "process-services-cloud" folder: "process-services-cloud/process" diff --git a/.vscode/launch.json b/.vscode/launch.json index fb054eac833..08f11965580 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "name": "e2e", "program": "${workspaceFolder}/node_modules/protractor/bin/protractor", "args": [ - "`${workspaceFolder}/.vscode/closest-config-finder.sh ${file} e2e/protractor.conf.js`", + "./e2e/protractor.conf.js", "--specs=${file}" ], "envFile": "${workspaceFolder}/.env", diff --git a/cspell.json b/cspell.json index 885040b502b..9e3dd28159f 100644 --- a/cspell.json +++ b/cspell.json @@ -141,7 +141,8 @@ "webscript", "Whitespaces", "xdescribe", - "xsrf" + "xsrf", + "BPMECM" ], "dictionaries": [ "html", diff --git a/demo-shell/src/app/app.component.ts b/demo-shell/src/app/app.component.ts index 416e56d1092..2273758e1d6 100644 --- a/demo-shell/src/app/app.component.ts +++ b/demo-shell/src/app/app.component.ts @@ -18,11 +18,11 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { AuthenticationService, - AlfrescoApiService, PageTitleService } from '@alfresco/adf-core'; import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; +import { AdfHttpClient } from '@alfresco/adf-core/api'; @Component({ selector: 'app-root', @@ -33,7 +33,7 @@ import { MatDialog } from '@angular/material/dialog'; export class AppComponent implements OnInit { constructor(private pageTitleService: PageTitleService, - private alfrescoApiService: AlfrescoApiService, + private adfHttpClient: AdfHttpClient, private authenticationService: AuthenticationService, private router: Router, private dialogRef: MatDialog) { @@ -43,7 +43,7 @@ export class AppComponent implements OnInit { ngOnInit() { this.pageTitleService.setTitle('title'); - this.alfrescoApiService.getInstance().on('error', (error) => { + this.adfHttpClient.on('error', (error) => { if (error.status === 401) { if (!this.authenticationService.isLoggedIn()) { this.dialogRef.closeAll(); diff --git a/demo-shell/src/app/app.module.ts b/demo-shell/src/app/app.module.ts index d911d172ddf..2822b58c512 100644 --- a/demo-shell/src/app/app.module.ts +++ b/demo-shell/src/app/app.module.ts @@ -74,7 +74,7 @@ import { UserInfoComponent } from './components/app-layout/user-info/user-info.c environment.e2e ? NoopAnimationsModule : BrowserAnimationsModule, ReactiveFormsModule, RouterModule.forRoot(appRoutes, { useHash: true, relativeLinkResolution: 'legacy' }), - ...(environment.oidc ? [AuthModule.forRoot({ useHash: true })] : []), + AuthModule.forRoot({ useHash: true }), FormsModule, HttpClientModule, MaterialModule, diff --git a/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts b/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts index 4d29c422942..894e4ba3291 100644 --- a/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts +++ b/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts @@ -17,7 +17,13 @@ import { EcmUserModel, PeopleContentService } from '@alfresco/adf-content-services'; import { BpmUserModel, PeopleProcessService } from '@alfresco/adf-process-services'; -import { AuthenticationService, IdentityUserModel, IdentityUserService, UserInfoMode } from '@alfresco/adf-core'; +import { + AuthenticationService, + BasicAlfrescoAuthService, + IdentityUserModel, + IdentityUserService, + UserInfoMode +} from '@alfresco/adf-core'; import { Component, OnInit, Input } from '@angular/core'; import { MenuPositionX, MenuPositionY } from '@angular/material/menu'; import { Observable, of } from 'rxjs'; @@ -46,6 +52,7 @@ export class UserInfoComponent implements OnInit { constructor(private peopleContentService: PeopleContentService, private peopleProcessService: PeopleProcessService, private identityUserService: IdentityUserService, + private basicAlfrescoAuthService: BasicAlfrescoAuthService, private authService: AuthenticationService) { } @@ -77,7 +84,7 @@ export class UserInfoComponent implements OnInit { } get isLoggedIn(): boolean { - if (this.authService.isKerberosEnabled()) { + if (this.basicAlfrescoAuthService.isKerberosEnabled()) { return true; } return this.authService.isLoggedIn(); @@ -96,15 +103,15 @@ export class UserInfoComponent implements OnInit { } private isAllLoggedIn() { - return (this.authService.isEcmLoggedIn() && this.authService.isBpmLoggedIn()) || (this.authService.isALLProvider() && this.authService.isKerberosEnabled()); + return (this.authService.isEcmLoggedIn() && this.authService.isBpmLoggedIn()) || (this.authService.isALLProvider() && this.basicAlfrescoAuthService.isKerberosEnabled()); } private isBpmLoggedIn() { - return this.authService.isBpmLoggedIn() || (this.authService.isECMProvider() && this.authService.isKerberosEnabled()); + return this.authService.isBpmLoggedIn() || (this.authService.isECMProvider() && this.basicAlfrescoAuthService.isKerberosEnabled()); } private isEcmLoggedIn() { - return this.authService.isEcmLoggedIn() || (this.authService.isECMProvider() && this.authService.isKerberosEnabled()); + return this.authService.isEcmLoggedIn() || (this.authService.isECMProvider() && this.basicAlfrescoAuthService.isKerberosEnabled()); } } diff --git a/demo-shell/src/app/components/settings/host-settings.component.html b/demo-shell/src/app/components/settings/host-settings.component.html index 3eaa05348d6..9207275c67c 100644 --- a/demo-shell/src/app/components/settings/host-settings.component.html +++ b/demo-shell/src/app/components/settings/host-settings.component.html @@ -71,7 +71,7 @@ - + Code Flow diff --git a/demo-shell/src/app/components/settings/host-settings.component.ts b/demo-shell/src/app/components/settings/host-settings.component.ts index d7f7f9dfb50..601a21d3580 100644 --- a/demo-shell/src/app/components/settings/host-settings.component.ts +++ b/demo-shell/src/app/components/settings/host-settings.component.ts @@ -65,7 +65,7 @@ export class HostSettingsComponent implements OnInit { private storageService: StorageService, private alfrescoApiService: AlfrescoApiService, private appConfig: AppConfigService, - private auth: AuthenticationService + private authenticationService: AuthenticationService ) {} ngOnInit() { @@ -191,8 +191,8 @@ export class HostSettingsComponent implements OnInit { this.storageService.setItem(AppConfigValues.AUTHTYPE, values.authType); this.alfrescoApiService.reset(); - this.auth.reset(); - this.alfrescoApiService.getInstance().invalidateSession(); + this.authenticationService.reset(); + this.authenticationService.logout(); this.success.emit(true); } @@ -235,10 +235,6 @@ export class HostSettingsComponent implements OnInit { return this.form.get('authType').value === 'OAUTH'; } - get supportsCodeFlow(): boolean { - return this.auth.supportCodeFlow; - } - get providersControl(): UntypedFormControl { return this.form.get('providersControl') as UntypedFormControl; } diff --git a/e2e/core/viewer/viewer-content-services-component.e2e.ts b/e2e/content-services/components/viewer-content-services-component.e2e.ts similarity index 99% rename from e2e/core/viewer/viewer-content-services-component.e2e.ts rename to e2e/content-services/components/viewer-content-services-component.e2e.ts index d3d24a9c21d..f38cdb292c4 100644 --- a/e2e/core/viewer/viewer-content-services-component.e2e.ts +++ b/e2e/content-services/components/viewer-content-services-component.e2e.ts @@ -21,7 +21,7 @@ import { ContentServicesPage } from '../../core/pages/content-services.page'; import { FileModel } from '../../models/ACS/file.model'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; import { VersionManagePage } from '../pages/version-manager.page'; -import { MetadataViewPage } from '../pages/metadata-view.page'; +import { MetadataViewPage } from '../../core/pages/metadata-view.page'; describe('Content Services Viewer', () => { const acsUser = new UserModel(); diff --git a/e2e/content-services/components/viewer-vesion.e2e.ts b/e2e/content-services/components/viewer-vesion.e2e.ts new file mode 100644 index 00000000000..635246b01cc --- /dev/null +++ b/e2e/content-services/components/viewer-vesion.e2e.ts @@ -0,0 +1,94 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; +import { createApiService, FileBrowserUtil, LoginPage, UploadActions, UserModel, UsersActions, ViewerPage } from '@alfresco/adf-testing'; +import { ContentServicesPage } from '../../core/pages/content-services.page'; +import { FileModel } from '../../models/ACS/file.model'; +import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; +import { VersionManagePage } from '../pages/version-manager.page'; + +describe('Viewer', () => { + + const navigationBarPage = new NavigationBarPage(); + const viewerPage = new ViewerPage(); + const loginPage = new LoginPage(); + const contentServicesPage = new ContentServicesPage(); + + const apiService = createApiService(); + const uploadActions = new UploadActions(apiService); + const usersActions = new UsersActions(apiService); + + const versionManagePage = new VersionManagePage(); + const acsUser = new UserModel(); + let txtFileUploaded; + + const txtFileInfo = new FileModel({ + name: browser.params.resources.Files.ADF_DOCUMENTS.TXT.file_name, + location: browser.params.resources.Files.ADF_DOCUMENTS.TXT.file_path + }); + + const fileModelVersionTwo = new FileModel({ + name: browser.params.resources.Files.ADF_DOCUMENTS.TXT.file_name, + location: browser.params.resources.Files.ADF_DOCUMENTS.TXT.file_location + }); + + beforeAll(async () => { + await apiService.loginWithProfile('admin'); + await usersActions.createUser(acsUser); + + await apiService.login(acsUser.username, acsUser.password); + + txtFileUploaded = await uploadActions.uploadFile(txtFileInfo.location, txtFileInfo.name, '-my-'); + + await loginPage.login(acsUser.username, acsUser.password); + }); + + afterAll(async () => { + await apiService.loginWithProfile('admin'); + await uploadActions.deleteFileOrFolder(txtFileUploaded.entry.id); + await navigationBarPage.clickLogoutButton(); + }); + + beforeEach(async () => { + await contentServicesPage.goToDocumentList(); + await contentServicesPage.doubleClickRow(txtFileUploaded.entry.name); + await viewerPage.waitTillContentLoaded(); + }); + + afterEach(async () => { + await viewerPage.clickCloseButton(); + }); + + it('[C362242] Should the Viewer be able to view a previous version of a file', async () => { + await contentServicesPage.versionManagerContent(txtFileInfo.name); + await versionManagePage.showNewVersionButton.click(); + await versionManagePage.uploadNewVersionFile(fileModelVersionTwo.location); + await versionManagePage.closeVersionDialog(); + await contentServicesPage.doubleClickRow(txtFileUploaded.entry.name); + await viewerPage.waitTillContentLoaded(); + await viewerPage.clickInfoButton(); + await viewerPage.clickOnTab('Versions'); + await versionManagePage.viewFileVersion('1.0'); + await viewerPage.expectUrlToContain('1.0'); + }); + + it('[C362265] Should the Viewer be able to download a previous version of a file', async () => { + await viewerPage.clickDownloadButton(); + await FileBrowserUtil.isFileDownloaded(txtFileInfo.name); + }); +}); diff --git a/e2e/content-services/document-list/document-list-pagination.e2e.ts b/e2e/content-services/document-list/document-list-pagination.e2e.ts index 97bf66840c4..bef68ea4758 100644 --- a/e2e/content-services/document-list/document-list-pagination.e2e.ts +++ b/e2e/content-services/document-list/document-list-pagination.e2e.ts @@ -31,7 +31,7 @@ import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; import { FolderModel } from '../../models/ACS/folder.model'; import { browser } from 'protractor'; import { FileModel } from '../../models/ACS/file.model'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; describe('Document List - Pagination', () => { const pagination = { diff --git a/e2e/core/pages/dialog/upload-dialog.page.ts b/e2e/content-services/pages/upload-dialog.page.ts similarity index 100% rename from e2e/core/pages/dialog/upload-dialog.page.ts rename to e2e/content-services/pages/upload-dialog.page.ts diff --git a/e2e/core/pages/version-manager.page.ts b/e2e/content-services/pages/version-manager.page.ts similarity index 100% rename from e2e/core/pages/version-manager.page.ts rename to e2e/content-services/pages/version-manager.page.ts diff --git a/e2e/content-services/upload/cancel-upload.e2e.ts b/e2e/content-services/upload/cancel-upload.e2e.ts index 996ff6f5783..a3e68a6dd02 100644 --- a/e2e/content-services/upload/cancel-upload.e2e.ts +++ b/e2e/content-services/upload/cancel-upload.e2e.ts @@ -18,7 +18,7 @@ import { browser } from 'protractor'; import { createApiService, LoginPage, UploadActions, UserModel, UsersActions } from '@alfresco/adf-testing'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; import { UploadTogglesPage } from '../../core/pages/dialog/upload-toggles.page'; import { FileModel } from '../../models/ACS/file.model'; diff --git a/e2e/content-services/upload/excluded-file.e2e.ts b/e2e/content-services/upload/excluded-file.e2e.ts index ad79d893522..08622a54ae6 100644 --- a/e2e/content-services/upload/excluded-file.e2e.ts +++ b/e2e/content-services/upload/excluded-file.e2e.ts @@ -24,7 +24,7 @@ import { createApiService, UsersActions } from '@alfresco/adf-testing'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; import { UploadTogglesPage } from '../../core/pages/dialog/upload-toggles.page'; import { FileModel } from '../../models/ACS/file.model'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; diff --git a/e2e/content-services/upload/upload-dialog.e2e.ts b/e2e/content-services/upload/upload-dialog.e2e.ts index 6640b58253b..9265c572cf9 100644 --- a/e2e/content-services/upload/upload-dialog.e2e.ts +++ b/e2e/content-services/upload/upload-dialog.e2e.ts @@ -17,11 +17,11 @@ import { createApiService, LoginPage, UploadActions, UserModel, UsersActions } from '@alfresco/adf-testing'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; import { UploadTogglesPage } from '../../core/pages/dialog/upload-toggles.page'; import { FileModel } from '../../models/ACS/file.model'; import { browser } from 'protractor'; -import { VersionManagePage } from '../../core/pages/version-manager.page'; +import { VersionManagePage } from '../pages/version-manager.page'; describe('Upload component', () => { diff --git a/e2e/content-services/upload/uploader-component.e2e.ts b/e2e/content-services/upload/uploader-component.e2e.ts index 2e6d94a5b00..80b80a8caff 100644 --- a/e2e/content-services/upload/uploader-component.e2e.ts +++ b/e2e/content-services/upload/uploader-component.e2e.ts @@ -19,7 +19,7 @@ import { browser, by, element } from 'protractor'; import { createApiService, DropActions, LoginPage, StringUtil, UploadActions, UserModel, UsersActions } from '@alfresco/adf-testing'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; import { UploadTogglesPage } from '../../core/pages/dialog/upload-toggles.page'; import { FileModel } from '../../models/ACS/file.model'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; diff --git a/e2e/content-services/upload/user-permission.e2e.ts b/e2e/content-services/upload/user-permission.e2e.ts index ac9fe598a9c..8a952eec686 100644 --- a/e2e/content-services/upload/user-permission.e2e.ts +++ b/e2e/content-services/upload/user-permission.e2e.ts @@ -18,7 +18,7 @@ import { browser } from 'protractor'; import { createApiService, LoginPage, SnackbarPage, StringUtil, UserModel, UsersActions } from '@alfresco/adf-testing'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; import { FileModel } from '../../models/ACS/file.model'; import CONSTANTS = require('../../util/constants'); diff --git a/e2e/content-services/upload/version-actions.e2e.ts b/e2e/content-services/upload/version-actions.e2e.ts index 043cb68d0d7..9bcd9cd3712 100644 --- a/e2e/content-services/upload/version-actions.e2e.ts +++ b/e2e/content-services/upload/version-actions.e2e.ts @@ -28,9 +28,9 @@ import { createApiService, import { browser, by, element } from 'protractor'; import { FileModel } from '../../models/ACS/file.model'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; -import { VersionManagePage } from '../../core/pages/version-manager.page'; +import { VersionManagePage } from '../pages/version-manager.page'; describe('Version component actions', () => { diff --git a/e2e/content-services/upload/version-permissions.e2e.ts b/e2e/content-services/upload/version-permissions.e2e.ts index 5203df79635..7dae21aaf7e 100644 --- a/e2e/content-services/upload/version-permissions.e2e.ts +++ b/e2e/content-services/upload/version-permissions.e2e.ts @@ -26,8 +26,8 @@ import { UsersActions } from '@alfresco/adf-testing'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; -import { VersionManagePage } from '../../core/pages/version-manager.page'; -import { UploadDialogPage } from '../../core/pages/dialog/upload-dialog.page'; +import { VersionManagePage } from '../pages/version-manager.page'; +import { UploadDialogPage } from '../pages/upload-dialog.page'; import { ContentServicesPage } from '../../core/pages/content-services.page'; import { FileModel } from '../../models/ACS/file.model'; import CONSTANTS = require('../../util/constants'); diff --git a/e2e/content-services/upload/version-properties.e2e.ts b/e2e/content-services/upload/version-properties.e2e.ts index 726fbc2ee0e..2430500a48d 100644 --- a/e2e/content-services/upload/version-properties.e2e.ts +++ b/e2e/content-services/upload/version-properties.e2e.ts @@ -24,7 +24,7 @@ import { createApiService, UsersActions, ViewerPage } from '@alfresco/adf-testing'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { VersionManagePage } from '../../core/pages/version-manager.page'; +import { VersionManagePage } from '../pages/version-manager.page'; import { FileModel } from '../../models/ACS/file.model'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; diff --git a/e2e/content-services/upload/version-smoke-tests.e2e.ts b/e2e/content-services/upload/version-smoke-tests.e2e.ts index c6f5439b096..3b6930be86f 100644 --- a/e2e/content-services/upload/version-smoke-tests.e2e.ts +++ b/e2e/content-services/upload/version-smoke-tests.e2e.ts @@ -18,7 +18,7 @@ import { browser } from 'protractor'; import { createApiService, LoginPage, UploadActions, UserModel, UsersActions } from '@alfresco/adf-testing'; import { ContentServicesPage } from '../../core/pages/content-services.page'; -import { VersionManagePage } from '../../core/pages/version-manager.page'; +import { VersionManagePage } from '../pages/version-manager.page'; import { FileModel } from '../../models/ACS/file.model'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; diff --git a/e2e/process-services-cloud/task-list/apps-section-cloud.e2e.ts b/e2e/process-services-cloud/task-list/apps-section-cloud.e2e.ts deleted file mode 100644 index 59b56644747..00000000000 --- a/e2e/process-services-cloud/task-list/apps-section-cloud.e2e.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createApiService, Application, AppListCloudPage, IdentityService, LocalStorageUtil, LoginPage } from '@alfresco/adf-testing'; -import { browser } from 'protractor'; -import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; - -describe('Applications list', () => { - - const simpleApp = browser.params.resources.ACTIVITI_CLOUD_APPS.SIMPLE_APP.name; - - const loginSSOPage = new LoginPage(); - const navigationBarPage = new NavigationBarPage(); - const appListCloudPage = new AppListCloudPage(); - - const apiService = createApiService(); - const applicationsService = new Application(apiService); - const identityService = new IdentityService(apiService); - - let testUser; - const appNames = []; - - beforeAll(async () => { - await apiService.loginWithProfile('identityAdmin'); - testUser = await identityService.createIdentityUserWithRole( [identityService.ROLES.ACTIVITI_USER, identityService.ROLES.ACTIVITI_DEVOPS]); - - await loginSSOPage.login(testUser.username, testUser.password); - await apiService.login(testUser.username, testUser.password); - - const applications = await applicationsService.getApplicationsByStatus('RUNNING'); - - applications.list.entries.forEach(app => { - appNames.push(app.entry.name.toLowerCase()); - }); - - await LocalStorageUtil.setConfigField('alfresco-deployed-apps', '[]'); - await LocalStorageUtil.apiReset(); - }); - - afterAll(async () => { - await apiService.loginWithProfile('identityAdmin'); - await identityService.deleteIdentityUser(testUser.idIdentityService); - }); - - it('[C310373] Should all the app with running state be displayed on dashboard when alfresco-deployed-apps is not used in config file', async () => { - await navigationBarPage.navigateToProcessServicesCloudPage(); - await appListCloudPage.checkApsContainer(); - - const list = await appListCloudPage.getNameOfTheApplications(); - - await expect(JSON.stringify(list)).toEqual(JSON.stringify(appNames)); - }); - - it('[C289910] Should the app be displayed on dashboard when is deployed on APS', async () => { - await browser.refresh(); - await navigationBarPage.navigateToProcessServicesCloudPage(); - await appListCloudPage.checkApsContainer(); - - await appListCloudPage.checkAppIsDisplayed(simpleApp); - await appListCloudPage.checkAppIsDisplayed(browser.params.resources.ACTIVITI_CLOUD_APPS.CANDIDATE_BASE_APP.name); - await appListCloudPage.checkAppIsDisplayed(browser.params.resources.ACTIVITI_CLOUD_APPS.SUB_PROCESS_APP.name); - - await expect(await appListCloudPage.countAllApps()).toEqual(3); - }); -}); diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js index 729ca318c0d..2d0c6a51611 100644 --- a/e2e/protractor.conf.js +++ b/e2e/protractor.conf.js @@ -285,7 +285,7 @@ exports.config = { // @ts-ignore if (browser.params.testConfig.appConfig.authType === 'OAUTH') { - + Logger.info(`Configure demo shell OAUTH`); // @ts-ignore await LocalStorageUtil.setStorageItem('identityHost', browser.params.testConfig.appConfig.identityHost); // @ts-ignore diff --git a/e2e/search/components/search-number-range.e2e.ts b/e2e/search/components/search-number-range.e2e.ts index 5223f730454..58a9b679076 100644 --- a/e2e/search/components/search-number-range.e2e.ts +++ b/e2e/search/components/search-number-range.e2e.ts @@ -270,7 +270,7 @@ describe('Search Number Range Filter', () => { for (const currentResult of results) { const currentSize = await BrowserActions.getAttribute(currentResult, 'title'); if (currentSize && currentSize.trim() !== '') { - await expect(currentSize === '0').toBe(true); + await expect((currentSize === '0' || currentSize === '1')).toBe(true); } } }); diff --git a/e2e/test.config.js b/e2e/test.config.js index bd1effdba93..34bab520162 100644 --- a/e2e/test.config.js +++ b/e2e/test.config.js @@ -9,14 +9,14 @@ const HOST = process.env.URL_HOST_ADF; const LOG = process.env.E2E_LOG_LEVEL; -const HOST_ECM = process.env.PROXY_HOST_ECM || HOST || 'ecm'; -const HOST_BPM = process.env.PROXY_HOST_BPM || HOST || 'bpm'; +const HOST_ECM = process.env.PROXY_HOST_ECM || process.env.PROXY_HOST_ADF || HOST || 'ecm'; +const HOST_BPM = process.env.PROXY_HOST_BPM || process.env.PROXY_HOST_ADF || HOST || 'bpm'; +const HOST_SSO = process.env.HOST_SSO || process.env.PROXY_HOST_ADF || HOST || 'oauth'; +const IDENTITY_HOST = process.env.IDENTITY_HOST || process.env.HOST_SSO + '/auth/admin/realms/alfresco'; const PROVIDER = process.env.PROVIDER ? process.env.PROVIDER : 'ALL'; const AUTH_TYPE = process.env.AUTH_TYPE ? process.env.AUTH_TYPE : 'BASIC'; -const HOST_SSO = process.env.HOST_SSO || process.env.PROXY_HOST_ADF || HOST || 'oauth'; -const IDENTITY_HOST = process.env.IDENTITY_HOST || process.env.HOST_SSO + '/auth/admin/realms/alfresco'; const OAUTH_CLIENT_ID = process.env.OAUTH_CLIENDID || 'alfresco'; const IDENTITY_ADMIN_EMAIL = process.env.IDENTITY_ADMIN_EMAIL || "defaultadmin"; diff --git a/lib/content-services/src/lib/common/services/content.service.spec.ts b/lib/content-services/src/lib/common/services/content.service.spec.ts index c2a7dadce5d..74e38254125 100644 --- a/lib/content-services/src/lib/common/services/content.service.spec.ts +++ b/lib/content-services/src/lib/common/services/content.service.spec.ts @@ -21,16 +21,11 @@ import { AppConfigService, AuthenticationService, StorageService, CoreTestingMod import { Node, PermissionsInfo } from '@alfresco/js-api'; import { TranslateModule } from '@ngx-translate/core'; -declare let jasmine: any; - describe('ContentService', () => { let contentService: ContentService; let authService: AuthenticationService; let storage: StorageService; - let node: any; - - const nodeId = 'fake-node-id'; beforeEach(() => { TestBed.configureTestingModule({ @@ -44,14 +39,6 @@ describe('ContentService', () => { storage = TestBed.inject(StorageService); storage.clear(); - node = { - entry: { - id: nodeId - } - }; - - jasmine.Ajax.install(); - const appConfig: AppConfigService = TestBed.inject(AppConfigService); appConfig.config = { ecmHost: 'http://localhost:9876/ecm', @@ -59,24 +46,6 @@ describe('ContentService', () => { }; }); - afterEach(() => { - jasmine.Ajax.uninstall(); - }); - - it('should return a valid content URL', (done) => { - authService.login('fake-username', 'fake-password').subscribe(() => { - expect(contentService.getContentUrl(node)).toContain('/ecm/alfresco/api/' + - '-default-/public/alfresco/versions/1/nodes/fake-node-id/content?attachment=false&alf_ticket=fake-post-ticket'); - done(); - }); - - jasmine.Ajax.requests.mostRecent().respondWith({ - status: 201, - contentType: 'application/json', - responseText: JSON.stringify({ entry: { id: 'fake-post-ticket', userId: 'admin' } }) - }); - }); - describe('AllowableOperations', () => { it('should hasAllowableOperations be false if allowableOperation is not present in the node', () => { diff --git a/lib/content-services/src/lib/common/services/discovery-api.service.ts b/lib/content-services/src/lib/common/services/discovery-api.service.ts index a5db003450c..8cefad2f7cb 100644 --- a/lib/content-services/src/lib/common/services/discovery-api.service.ts +++ b/lib/content-services/src/lib/common/services/discovery-api.service.ts @@ -18,16 +18,27 @@ import { Injectable } from '@angular/core'; import { from, Observable, throwError, Subject } from 'rxjs'; import { catchError, map, switchMap, filter, take } from 'rxjs/operators'; -import { RepositoryInfo, SystemPropertiesRepresentation } from '@alfresco/js-api'; +import { + RepositoryInfo, + SystemPropertiesRepresentation, + DiscoveryApi, + AboutApi, + SystemPropertiesApi +} from '@alfresco/js-api'; -import { BpmProductVersionModel, AuthenticationService } from '@alfresco/adf-core'; -import { ApiClientsService } from '@alfresco/adf-core/api'; +import { AlfrescoApiService, BpmProductVersionModel, AuthenticationService } from '@alfresco/adf-core'; @Injectable({ providedIn: 'root' }) export class DiscoveryApiService { + private _discoveryApi: DiscoveryApi; + get discoveryApi(): DiscoveryApi { + this._discoveryApi = this._discoveryApi ?? new DiscoveryApi(this.alfrescoApiService.getInstance()); + return this._discoveryApi; + } + /** * Gets product information for Content Services. */ @@ -35,15 +46,17 @@ export class DiscoveryApiService { constructor( private authenticationService: AuthenticationService, - private apiClientsService: ApiClientsService + private alfrescoApiService: AlfrescoApiService ) { - this.authenticationService.onLogin - .pipe( + this.authenticationService.onLogin.subscribe(() => { + this.alfrescoApiService.alfrescoApiInitialized.pipe( filter(() => this.authenticationService.isEcmLoggedIn()), take(1), switchMap(() => this.getEcmProductInfo()) ) - .subscribe((info) => this.ecmProductInfo$.next(info)); + .subscribe((info) => this.ecmProductInfo$.next(info)); + + }); } @@ -53,9 +66,8 @@ export class DiscoveryApiService { * @returns ProductVersionModel containing product details */ getEcmProductInfo(): Observable { - const discoveryApi = this.apiClientsService.get('DiscoveryClient.discovery'); - return from(discoveryApi.getRepositoryInformation()) + return from(this.discoveryApi.getRepositoryInformation()) .pipe( map((res) => res.entry.repository), catchError((err) => throwError(err)) @@ -68,7 +80,7 @@ export class DiscoveryApiService { * @returns ProductVersionModel containing product details */ getBpmProductInfo(): Observable { - const aboutApi = this.apiClientsService.get('ActivitiClient.about'); + const aboutApi = new AboutApi(this.alfrescoApiService.getInstance()); return from(aboutApi.getAppVersion()) .pipe( @@ -78,7 +90,7 @@ export class DiscoveryApiService { } getBPMSystemProperties(): Observable { - const systemPropertiesApi = this.apiClientsService.get('ActivitiClient.system-properties'); + const systemPropertiesApi = new SystemPropertiesApi(this.alfrescoApiService.getInstance()); return from(systemPropertiesApi.getProperties()) .pipe( diff --git a/lib/content-services/src/lib/common/services/people-content.service.spec.ts b/lib/content-services/src/lib/common/services/people-content.service.spec.ts index f8efb236d7b..3bc542fa8c0 100644 --- a/lib/content-services/src/lib/common/services/people-content.service.spec.ts +++ b/lib/content-services/src/lib/common/services/people-content.service.spec.ts @@ -25,7 +25,6 @@ import { import { AlfrescoApiService, AlfrescoApiServiceMock, - AuthenticationService, CoreTestingModule } from '@alfresco/adf-core'; import { PeopleContentQueryRequestModel, PeopleContentService } from './people-content.service'; @@ -34,7 +33,6 @@ import { TestBed } from '@angular/core/testing'; describe('PeopleContentService', () => { let peopleContentService: PeopleContentService; - let authenticationService: AuthenticationService; beforeEach(() => { TestBed.configureTestingModule({ @@ -47,7 +45,6 @@ describe('PeopleContentService', () => { ] }); - authenticationService = TestBed.inject(AuthenticationService); peopleContentService = TestBed.inject(PeopleContentService); }); @@ -130,17 +127,6 @@ describe('PeopleContentService', () => { expect(getCurrentPersonSpy.calls.count()).toEqual(1); }); - it('should reset the admin cache upon logout', async () => { - spyOn(peopleContentService.peopleApi, 'getPerson').and.returnValue(Promise.resolve({ entry: fakeEcmAdminUser } as any)); - - const user = await peopleContentService.getCurrentUserInfo().toPromise(); - expect(user.id).toEqual('fake-id'); - expect(peopleContentService.isCurrentUserAdmin()).toBe(true); - - authenticationService.onLogout.next(true); - expect(peopleContentService.isCurrentUserAdmin()).toBe(false); - }); - it('should not change current user on every getPerson call', async () => { const getCurrentPersonSpy = spyOn(peopleContentService.peopleApi, 'getPerson').and.returnValue(Promise.resolve({entry: fakeEcmAdminUser} as any)); await peopleContentService.getCurrentUserInfo().toPromise(); diff --git a/lib/content-services/src/lib/directives/library-favorite.directive.spec.ts b/lib/content-services/src/lib/directives/library-favorite.directive.spec.ts index 9af3a8f133c..f9082116797 100644 --- a/lib/content-services/src/lib/directives/library-favorite.directive.spec.ts +++ b/lib/content-services/src/lib/directives/library-favorite.directive.spec.ts @@ -18,8 +18,7 @@ import { Component, ViewChild } from '@angular/core'; import { LibraryFavoriteDirective } from './library-favorite.directive'; import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { AlfrescoApiServiceMock, CoreModule, AlfrescoApiService } from '@alfresco/adf-core'; +import { CoreTestingModule } from '@alfresco/adf-core'; import { LibraryEntity } from '../interfaces/library-entity.interface'; @Component({ @@ -40,10 +39,7 @@ describe('LibraryFavoriteDirective', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), CoreModule.forRoot()], - providers: [ - { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock } - ], + imports: [CoreTestingModule], declarations: [TestComponent, LibraryFavoriteDirective] }); fixture = TestBed.createComponent(TestComponent); diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts index 9ee89ac841d..00adf1b0942 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts @@ -18,7 +18,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange, QueryList, Component, ViewChild, SimpleChanges } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { - AlfrescoApiService, DataColumnListComponent, DataColumnComponent, DataColumn, @@ -27,7 +26,8 @@ import { ObjectDataTableAdapter, ShowHeaderMode, ThumbnailService, - AppConfigService + AppConfigService, + AuthenticationService } from '@alfresco/adf-core'; import { ContentService } from '../../common/services/content.service'; import { Subject, of, throwError } from 'rxjs'; @@ -70,7 +70,6 @@ const mockDialog = { describe('DocumentList', () => { let documentList: DocumentListComponent; let documentListService: DocumentListService; - let apiService: AlfrescoApiService; let customResourcesService: CustomResourcesService; let thumbnailService: ThumbnailService; let contentService: ContentService; @@ -82,6 +81,7 @@ describe('DocumentList', () => { let spyFavorite: any; let spyFolder: any; let spyFolderNode: any; + let authenticationService: AuthenticationService; beforeEach(() => { TestBed.configureTestingModule({ @@ -99,11 +99,11 @@ describe('DocumentList', () => { documentList = fixture.componentInstance; documentListService = TestBed.inject(DocumentListService); - apiService = TestBed.inject(AlfrescoApiService); customResourcesService = TestBed.inject(CustomResourcesService); thumbnailService = TestBed.inject(ThumbnailService); contentService = TestBed.inject(ContentService); appConfigService = TestBed.inject(AppConfigService); + authenticationService = TestBed.inject(AuthenticationService); spyFolder = spyOn(documentListService, 'getFolder').and.returnValue(of({ list: {} })); spyFolderNode = spyOn(documentListService, 'getFolderNode').and.returnValue(of(new NodeEntry({ entry: new Node() }))); @@ -611,7 +611,7 @@ describe('DocumentList', () => { title: 'FileAction' }); - spyOn(apiService.getInstance(), 'getEcmUsername').and.returnValue('lockOwner'); + spyOn(authenticationService, 'getEcmUsername').and.returnValue('lockOwner'); documentList.actions = [documentMenu]; @@ -642,7 +642,7 @@ describe('DocumentList', () => { title: 'FileAction' }); - spyOn(apiService.getInstance(), 'getEcmUsername').and.returnValue('jerryTheKillerCow'); + spyOn(authenticationService, 'getEcmUsername').and.returnValue('jerryTheKillerCow'); documentList.actions = [documentMenu]; diff --git a/lib/content-services/src/lib/document-list/services/lock.service.spec.ts b/lib/content-services/src/lib/document-list/services/lock.service.spec.ts index dce5fdd7d98..f42dbe0eee2 100644 --- a/lib/content-services/src/lib/document-list/services/lock.service.spec.ts +++ b/lib/content-services/src/lib/document-list/services/lock.service.spec.ts @@ -17,7 +17,7 @@ import { TestBed } from '@angular/core/testing'; import { LockService } from './lock.service'; -import { CoreTestingModule, AlfrescoApiService } from '@alfresco/adf-core'; +import { CoreTestingModule, AuthenticationService } from '@alfresco/adf-core'; import { Node } from '@alfresco/js-api'; import { TranslateModule } from '@ngx-translate/core'; import { addDays, subDays } from 'date-fns'; @@ -25,7 +25,7 @@ import { addDays, subDays } from 'date-fns'; describe('PeopleProcessService', () => { let service: LockService; - let apiService: AlfrescoApiService; + let authenticationService: AuthenticationService; const fakeNodeUnlocked: Node = { name: 'unlocked', isLocked: false, isFile: true } as Node; const fakeFolderNode: Node = { name: 'unlocked', isLocked: false, isFile: false, isFolder: true } as Node; @@ -39,7 +39,7 @@ describe('PeopleProcessService', () => { ] }); service = TestBed.inject(LockService); - apiService = TestBed.inject(AlfrescoApiService); + authenticationService = TestBed.inject(AuthenticationService); }); it('should return false when no lock is configured', () => { @@ -145,22 +145,22 @@ describe('PeopleProcessService', () => { } as Node; it('should return false when the user is the lock owner', () => { - spyOn(apiService.getInstance(), 'getEcmUsername').and.returnValue('lock-owner-user'); + spyOn(authenticationService, 'getEcmUsername').and.returnValue('lock-owner-user'); expect(service.isLocked(nodeOwnerAllowedLock)).toBeFalsy(); }); it('should return true when the user is not the lock owner', () => { - spyOn(apiService.getInstance(), 'getEcmUsername').and.returnValue('banana-user'); + spyOn(authenticationService, 'getEcmUsername').and.returnValue('banana-user'); expect(service.isLocked(nodeOwnerAllowedLock)).toBeTruthy(); }); it('should return false when the user is not the lock owner but the lock is expired', () => { - spyOn(apiService.getInstance(), 'getEcmUsername').and.returnValue('banana-user'); + spyOn(authenticationService, 'getEcmUsername').and.returnValue('banana-user'); expect(service.isLocked(nodeOwnerAllowedLockWithExpiredDate)).toBeFalsy(); }); it('should return true when is not the lock owner and the expiration date is valid', () => { - spyOn(apiService.getInstance(), 'getEcmUsername').and.returnValue('banana-user'); + spyOn(authenticationService, 'getEcmUsername').and.returnValue('banana-user'); expect(service.isLocked(nodeOwnerAllowedLockWithActiveExpiration)).toBeTruthy(); }); }); diff --git a/lib/content-services/src/lib/testing/content.testing.module.ts b/lib/content-services/src/lib/testing/content.testing.module.ts index 2992be21501..e3a7c8bf542 100644 --- a/lib/content-services/src/lib/testing/content.testing.module.ts +++ b/lib/content-services/src/lib/testing/content.testing.module.ts @@ -27,7 +27,8 @@ import { AlfrescoApiServiceMock, AppConfigServiceMock, TranslationMock, - CookieServiceMock + CookieServiceMock, + AuthModule } from '@alfresco/adf-core'; import { ContentModule } from '../content.module'; import { TranslateModule } from '@ngx-translate/core'; @@ -37,6 +38,7 @@ import { MatIconTestingModule } from '@angular/material/icon/testing'; @NgModule({ imports: [ + AuthModule.forRoot({ useHash: true }), NoopAnimationsModule, RouterTestingModule, TranslateModule, diff --git a/lib/core/api/src/index.ts b/lib/core/api/src/index.ts index d65fe15513d..fe326e1e82e 100644 --- a/lib/core/api/src/index.ts +++ b/lib/core/api/src/index.ts @@ -15,9 +15,6 @@ * limitations under the License. */ -export * from './lib/api-client.factory'; -export * from './lib/api-clients.service'; -export * from './lib/clients'; export * from './lib/types'; export * from './lib/adf-http-client.service'; export * from './lib/interfaces'; diff --git a/lib/core/api/src/lib/adf-http-client.service.spec.ts b/lib/core/api/src/lib/adf-http-client.service.spec.ts index 09c99562633..19679584021 100644 --- a/lib/core/api/src/lib/adf-http-client.service.spec.ts +++ b/lib/core/api/src/lib/adf-http-client.service.spec.ts @@ -321,4 +321,86 @@ describe('AdfHttpClient', () => { req.flush(null, { status: 200, statusText: 'Ok' }); }); + it('should set Content-type to multipart/form-data if contentTypes array contains only multipart/form-data element', () => { + const options: RequestOptions = { + path: '', + httpMethod: 'POST', + contentTypes: ['multipart/form-data'], + queryParams: { + lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z') + } + }; + + angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error => + fail(error) + ); + + const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z'); + + expect(req.request.headers.get('Content-Type')).toEqual('multipart/form-data'); + + req.flush(null, { status: 200, statusText: 'Ok' }); + }); + + it('should set Content-type header to application/json if contentTypes array contains application/json', () => { + const options: RequestOptions = { + path: '', + httpMethod: 'POST', + contentTypes: ['multipart/form-data', 'application/json'], + queryParams: { + lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z') + } + }; + + angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error => + fail(error) + ); + + const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z'); + + expect(req.request.headers.get('Content-Type')).toEqual('application/json'); + + req.flush(null, { status: 200, statusText: 'Ok' }); + }); + + it('should set Content-type to application/json if contentTypes is not passed to the request options', () => { + const options: RequestOptions = { + path: '', + httpMethod: 'POST', + queryParams: { + lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z') + } + }; + + angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error => + fail(error) + ); + + const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z'); + + expect(req.request.headers.get('Content-Type')).toEqual('application/json'); + + req.flush(null, { status: 200, statusText: 'Ok' }); + }); + + it('should set Accept header to application/json if accepts is not passed to the request options', () => { + const options: RequestOptions = { + path: '', + httpMethod: 'POST', + queryParams: { + lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z') + } + }; + + angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error => + fail(error) + ); + + const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z'); + + expect(req.request.headers.get('Accept')).toEqual('application/json'); + + req.flush(null, { status: 200, statusText: 'Ok' }); + }); + }); diff --git a/lib/core/api/src/lib/adf-http-client.service.ts b/lib/core/api/src/lib/adf-http-client.service.ts index 973bdaf333d..f000839817b 100644 --- a/lib/core/api/src/lib/adf-http-client.service.ts +++ b/lib/core/api/src/lib/adf-http-client.service.ts @@ -57,16 +57,9 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { on: ee.EmitterMethod; off: ee.EmitterMethod; once: ee.EmitterMethod; - emit: (type: string, ...args: any[]) => void; - - private _disableCsrf = false; + _disableCsrf: boolean; - private defaultSecurityOptions = { - withCredentials: true, - isBpmRequest: false, - authentications: {}, - defaultHeaders: {} - }; + emit: (type: string, ...args: any[]) => void; get disableCsrf(): boolean { return this._disableCsrf; @@ -76,8 +69,14 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { this._disableCsrf = disableCsrf; } - constructor(private httpClient: HttpClient - ) { + private defaultSecurityOptions = { + withCredentials: true, + isBpmRequest: false, + authentications: {}, + defaultHeaders: {} + }; + + constructor(private httpClient: HttpClient) { ee(this); } @@ -217,7 +216,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { } eventEmitter.emit('error', err); - apiClientEmitter.emit('error', err); + apiClientEmitter.emit('error', { ...err, response: { req: err } }); if (err.status === 401) { eventEmitter.emit('unauthorized'); @@ -232,10 +231,10 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { // for backwards compatibility to handle cases in code where we try read response.error.response.body; const error = { - response: {...err, body: err.error} + ...err, body: err.error }; - const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error.response); + const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error); return throwError(alfrescoApiError); }), takeUntil(abort$) @@ -252,7 +251,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { } private static getBody(options: RequestOptions): any { - const contentType = options.contentType; + const contentType = options.contentType ? options.contentType : AdfHttpClient.jsonPreferredMime(options.contentTypes); const isFormData = contentType === 'multipart/form-data'; const isFormUrlEncoded = contentType === 'application/x-www-form-urlencoded'; const body = options.bodyParam; @@ -269,20 +268,58 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { } private getHeaders(options: RequestOptions): HttpHeaders { + const contentType = options.contentType || AdfHttpClient.jsonPreferredMime(options.contentTypes); + const accept = options.accept || AdfHttpClient.jsonPreferredMime(options.accepts); + const optionsHeaders = { ...options.headerParams, - ...(options.accept && {Accept: options.accept}), - ...((options.contentType) && {'Content-Type': options.contentType}) + ...(accept && {Accept: accept}), + ...((contentType) && {'Content-Type': contentType}) }; if (!this.disableCsrf) { this.setCsrfToken(optionsHeaders); - } return new HttpHeaders(optionsHeaders); } + /** + * Chooses a content type from the given array, with JSON preferred; i.e. return JSON if included, otherwise return the first. + * + * @param contentTypes a contentType array + * @returns The chosen content type, preferring JSON. + */ + private static jsonPreferredMime(contentTypes: readonly string[]): string { + if (!contentTypes?.length) { + return 'application/json'; + } + + for (let i = 0; i < contentTypes.length; i++) { + if (AdfHttpClient.isJsonMime(contentTypes[i])) { + return contentTypes[i]; + } + } + return contentTypes[0]; + } + + /** + * Checks whether the given content type represents JSON.
+ * JSON content type examples:
+ *
    + *
  • application/json
  • + *
  • application/json; charset=UTF8
  • + *
  • APPLICATION/JSON
  • + *
+ * + * @param contentType The MIME content type to check. + * @returns true if contentType represents JSON, otherwise false. + */ + private static isJsonMime(contentType: string): boolean { + return Boolean(contentType?.match(/^application\/json(;.*)?$/i)); + } + + private setCsrfToken(optionsHeaders: any) { const token = this.createCSRFToken(); optionsHeaders['X-CSRF-TOKEN'] = token; diff --git a/lib/core/api/src/lib/api-client.factory.ts b/lib/core/api/src/lib/api-client.factory.ts deleted file mode 100644 index 6ddd1f8aef9..00000000000 --- a/lib/core/api/src/lib/api-client.factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { InjectionToken } from '@angular/core'; -import { Constructor } from './types'; - -export interface ApiClientFactory { - create(apiClass: Constructor): T; -} - -export const API_CLIENT_FACTORY_TOKEN = new InjectionToken('api-client-factory'); diff --git a/lib/core/api/src/lib/api-clients.service.spec.ts b/lib/core/api/src/lib/api-clients.service.spec.ts deleted file mode 100644 index c240b5db6b3..00000000000 --- a/lib/core/api/src/lib/api-clients.service.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AboutApi } from '@alfresco/js-api'; -import { TestBed } from '@angular/core/testing'; -import { ApiClientFactory, API_CLIENT_FACTORY_TOKEN } from './api-client.factory'; -import { ApiClientsService } from './api-clients.service'; -import { Constructor } from './types'; - -class MockApiClientFactory implements ApiClientFactory { - create(apiClass: Constructor): T { - return new apiClass(); - } -} - -describe('ApiService', () => { - let apiService: ApiClientsService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - ApiClientsService, - { provide: API_CLIENT_FACTORY_TOKEN, useClass: MockApiClientFactory } - ] - }); - apiService = TestBed.inject(ApiClientsService); - }); - - it('should add api to registry', () => { - apiService.register('ActivitiClient.about', AboutApi); - - expect(apiService.get('ActivitiClient.about') instanceof AboutApi).toBeTruthy(); - }); - - it('should throw error if we try to get unregisterd API', () => { - expect(() => apiService.get('ActivitiClient.about')).toThrowError(); - - apiService.register('ActivitiClient.about', AboutApi); - - expect(() => apiService.get('ActivitiClient.about')).not.toThrowError(); - }); - - it('should create only single instance of API', () => { - apiService.register('ActivitiClient.about', AboutApi); - - const a = apiService.get('ActivitiClient.about'); - const b = apiService.get('ActivitiClient.about'); - - expect(a).toBe(b); - }); - -}); diff --git a/lib/core/api/src/lib/api-clients.service.ts b/lib/core/api/src/lib/api-clients.service.ts deleted file mode 100644 index 3c01fc56fb9..00000000000 --- a/lib/core/api/src/lib/api-clients.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Inject, Injectable } from '@angular/core'; -import { ApiClientFactory, API_CLIENT_FACTORY_TOKEN } from './api-client.factory'; -import { Constructor, Dictionary } from './types'; - -/* eslint-disable */ - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace AlfrescoCore { - interface ApiRegistry { - } - } -} -/* eslint-enable */ - -@Injectable() -export class ApiClientsService { - - constructor(@Inject(API_CLIENT_FACTORY_TOKEN) private apiCreateFactory: ApiClientFactory) { - } - - private registry: Dictionary> = {}; - private instances: Partial = {}; - - get(apiName: T): AlfrescoCore.ApiRegistry[T] { - - const apiClass = this.registry[apiName]; - - if (!apiClass) { - throw new Error(`Api not registred: ${apiName}`); - } - - return this.instances[apiName] as AlfrescoCore.ApiRegistry[T] ?? this.instantiateApi(apiName); - } - - - register(apiName: T, api: Constructor): void { - this.registry[apiName] = api; - } - - private instantiateApi(apiName: T): AlfrescoCore.ApiRegistry[T] { - const apiClass = this.registry[apiName]; - const instance = this.apiCreateFactory.create(apiClass); - this.instances[apiName] = instance; - - return instance; - } -} - diff --git a/lib/core/api/src/lib/clients/activiti/activiti-client.module.ts b/lib/core/api/src/lib/clients/activiti/activiti-client.module.ts deleted file mode 100644 index f906aafdcdf..00000000000 --- a/lib/core/api/src/lib/clients/activiti/activiti-client.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AboutApi, SystemPropertiesApi } from '@alfresco/js-api'; -import { NgModule } from '@angular/core'; -import { ApiClientsService } from '../../api-clients.service'; - -@NgModule() -export class ActivitiClientModule { - constructor(private apiClientsService: ApiClientsService) { - this.apiClientsService.register('ActivitiClient.about', AboutApi); - this.apiClientsService.register('ActivitiClient.system-properties', SystemPropertiesApi); - } -} diff --git a/lib/core/api/src/lib/clients/activiti/activiti-client.types.ts b/lib/core/api/src/lib/clients/activiti/activiti-client.types.ts deleted file mode 100644 index 4a768c6c28c..00000000000 --- a/lib/core/api/src/lib/clients/activiti/activiti-client.types.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AboutApi, SystemPropertiesApi } from '@alfresco/js-api'; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace AlfrescoCore { - interface ApiRegistry { - ['ActivitiClient.about']: AboutApi; - ['ActivitiClient.system-properties']: SystemPropertiesApi; - } - } -} - diff --git a/lib/core/api/src/lib/clients/alfresco-js-clients.module.ts b/lib/core/api/src/lib/clients/alfresco-js-clients.module.ts deleted file mode 100644 index cf0ce913460..00000000000 --- a/lib/core/api/src/lib/clients/alfresco-js-clients.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { ApiClientsService } from '../api-clients.service'; -import { ActivitiClientModule } from './activiti/activiti-client.module'; -import { DiscoveryClientModule } from './discovery/discovery-client.module'; - -@NgModule({ - imports: [ - HttpClientModule, - HttpClientXsrfModule.withOptions({ - cookieName: 'CSRF-TOKEN', - headerName: 'X-CSRF-TOKEN' - }), - ActivitiClientModule, - DiscoveryClientModule - ], - providers: [ - ApiClientsService - ] -}) -export class AlfrescoJsClientsModule { } diff --git a/lib/core/api/src/lib/clients/discovery/discovery-client.module.ts b/lib/core/api/src/lib/clients/discovery/discovery-client.module.ts deleted file mode 100644 index 242e09620b3..00000000000 --- a/lib/core/api/src/lib/clients/discovery/discovery-client.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DiscoveryApi } from '@alfresco/js-api'; -import { NgModule } from '@angular/core'; -import { ApiClientsService } from '../../api-clients.service'; - -@NgModule() -export class DiscoveryClientModule { - constructor(private apiClientsService: ApiClientsService) { - this.apiClientsService.register('DiscoveryClient.discovery', DiscoveryApi); - } -} diff --git a/lib/core/api/src/lib/clients/discovery/discovery-client.types.ts b/lib/core/api/src/lib/clients/discovery/discovery-client.types.ts deleted file mode 100644 index 64c7f6e52b0..00000000000 --- a/lib/core/api/src/lib/clients/discovery/discovery-client.types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DiscoveryApi } from '@alfresco/js-api'; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace AlfrescoCore { - interface ApiRegistry { - ['DiscoveryClient.discovery']: DiscoveryApi; - } - } -} diff --git a/lib/core/api/src/lib/interfaces.ts b/lib/core/api/src/lib/interfaces.ts index a4e3d043e34..de95500bc73 100644 --- a/lib/core/api/src/lib/interfaces.ts +++ b/lib/core/api/src/lib/interfaces.ts @@ -15,22 +15,41 @@ * limitations under the License. */ +export interface SecurityOptions { + readonly withCredentials?: boolean; + readonly authentications?: Authentication; + readonly defaultHeaders?: Record; +} + +export interface Oauth2 { + refreshToken?: string; + accessToken?: string; +} + +export interface BasicAuth { + username?: string; + password?: string; + ticket?: string; +} + +export interface Authentication { + basicAuth?: BasicAuth; + oauth2?: Oauth2; + cookie?: string; + type?: string; +} + export interface RequestOptions { httpMethod?: string; + pathParams?: any; queryParams?: any; headerParams?: any; formParams?: any; bodyParam?: any; returnType?: any; responseType?: string; + accepts?: string[]; + contentTypes?: string[]; readonly accept?: string; readonly contentType?: string; } - -export interface SecurityOptions { - readonly isBpmRequest: boolean; - readonly enableCsrf?: boolean; - readonly withCredentials?: boolean; - readonly authentications: any; - readonly defaultHeaders: Record; -} diff --git a/lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts index 18bb81aba64..07d0bdbe796 100644 --- a/lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts +++ b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts @@ -22,7 +22,7 @@ import { Authentication } from '../authentication'; import { AuthenticationInterceptor, SHOULD_ADD_AUTH_TOKEN } from './authentication.interceptor'; class MockAuthentication extends Authentication { - addTokenToHeader(httpHeaders: HttpHeaders): Observable { + addTokenToHeader(_: string, httpHeaders: HttpHeaders): Observable { return of(httpHeaders); } } diff --git a/lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts index 94be3ade924..8e194eb40b8 100644 --- a/lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts +++ b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts @@ -43,7 +43,7 @@ export class AuthenticationInterceptor implements HttpInterceptor { Observable | HttpUserEvent> { if (req.context.get(SHOULD_ADD_AUTH_TOKEN)) { - return this.authService.addTokenToHeader(req.headers).pipe( + return this.authService.addTokenToHeader(req.url, req.headers).pipe( mergeMap((headersWithBearer) => { const headerWithContentType = this.appendJsonContentType(headersWithBearer); const kcReq = req.clone({ headers: headerWithContentType}); diff --git a/lib/core/auth/src/authentication.ts b/lib/core/auth/src/authentication.ts index 05bf0026823..87da959e1a8 100644 --- a/lib/core/auth/src/authentication.ts +++ b/lib/core/auth/src/authentication.ts @@ -19,5 +19,5 @@ import { HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; export abstract class Authentication { - public abstract addTokenToHeader(headers: HttpHeaders): Observable; + public abstract addTokenToHeader(requestUrl: string, headers: HttpHeaders): Observable; } diff --git a/lib/core/src/lib/api-factories/alfresco-api-v2-loader.service.ts b/lib/core/src/lib/api-factories/alfresco-api-v2-loader.service.ts index 836c53d9b13..7c8b653102b 100644 --- a/lib/core/src/lib/api-factories/alfresco-api-v2-loader.service.ts +++ b/lib/core/src/lib/api-factories/alfresco-api-v2-loader.service.ts @@ -19,6 +19,8 @@ import { AlfrescoApiConfig } from '@alfresco/js-api'; import { Injectable } from '@angular/core'; import { AppConfigService, AppConfigValues } from '../app-config/app-config.service'; import { AlfrescoApiService } from '../services/alfresco-api.service'; +import { StorageService } from '../common/services/storage.service'; +import { AuthenticationService, BasicAlfrescoAuthService } from '../auth'; /** * Create a factory to resolve an api service instance @@ -34,10 +36,22 @@ export function createAlfrescoApiInstance(angularAlfrescoApiService: AlfrescoApi providedIn: 'root' }) export class AlfrescoApiLoaderService { - constructor(private readonly appConfig: AppConfigService, private readonly apiService: AlfrescoApiService) {} + constructor(private readonly appConfig: AppConfigService, + private readonly apiService: AlfrescoApiService, + private readonly basicAlfrescoAuthService: BasicAlfrescoAuthService, + private readonly authService: AuthenticationService, + private storageService: StorageService) { + } async init(): Promise { await this.appConfig.load(); + + this.authService.onLogin.subscribe(async () => { + if (this.authService.isOauth() && (this.authService.isALLProvider() || this.authService.isECMProvider())) { + await this.basicAlfrescoAuthService.requireAlfTicket(); + } + }); + return this.initAngularAlfrescoApi(); } @@ -59,6 +73,8 @@ export class AlfrescoApiLoaderService { disableCsrf: this.appConfig.get(AppConfigValues.DISABLECSRF), withCredentials: this.appConfig.get(AppConfigValues.AUTH_WITH_CREDENTIALS, false), domainPrefix: this.appConfig.get(AppConfigValues.STORAGE_PREFIX), + ticketEcm: this.storageService.getItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL), + ticketBpm: this.storageService.getItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL), oauth2: oauth }); diff --git a/lib/core/src/lib/api-factories/legacy-api-client.factory.ts b/lib/core/src/lib/api-factories/legacy-api-client.factory.ts deleted file mode 100644 index 7f87468678d..00000000000 --- a/lib/core/src/lib/api-factories/legacy-api-client.factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiClientFactory, Constructor } from '@alfresco/adf-core/api'; -import { Injectable } from '@angular/core'; -import { AlfrescoApiService } from '../services/alfresco-api.service'; - -@Injectable() -export class LegacyClientFactory implements ApiClientFactory { - constructor(private alfrescoApiService: AlfrescoApiService) { } - - create(apiClass: Constructor): T { - return new apiClass(this.alfrescoApiService.getInstance()); - } -} diff --git a/lib/core/src/lib/api-factories/legacy-api-client.module.ts b/lib/core/src/lib/api-factories/legacy-api-client.module.ts deleted file mode 100644 index e645fff5430..00000000000 --- a/lib/core/src/lib/api-factories/legacy-api-client.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { NgModule } from '@angular/core'; -import { API_CLIENT_FACTORY_TOKEN } from '@alfresco/adf-core/api'; -import { LegacyClientFactory } from './legacy-api-client.factory'; - - -@NgModule({ - providers: [ - { provide: API_CLIENT_FACTORY_TOKEN, useClass: LegacyClientFactory } - ] -}) -export class LegacyApiClientModule { } diff --git a/lib/core/src/lib/app-config/app-config.loader.ts b/lib/core/src/lib/app-config/app-config.loader.ts index d04bde43e11..11dde868f7b 100644 --- a/lib/core/src/lib/app-config/app-config.loader.ts +++ b/lib/core/src/lib/app-config/app-config.loader.ts @@ -28,8 +28,14 @@ import { AdfHttpClient } from '@alfresco/adf-core/api'; * @returns factory function */ export function loadAppConfig(appConfigService: AppConfigService, storageService: StorageService, adfHttpClient: AdfHttpClient) { - return () => appConfigService.load().then(() => { + + const init = () => { adfHttpClient.disableCsrf = appConfigService.get(AppConfigValues.DISABLECSRF, true); storageService.prefix = appConfigService.get(AppConfigValues.STORAGE_PREFIX, ''); - }); -} + + appConfigService.select(AppConfigValues.STORAGE_PREFIX).subscribe((property) => { + storageService.prefix = property; + }); + }; + return () => appConfigService.load(init); +}; diff --git a/lib/core/src/lib/app-config/app-config.service.spec.ts b/lib/core/src/lib/app-config/app-config.service.spec.ts index d7352fba3e5..a6b045da5fb 100644 --- a/lib/core/src/lib/app-config/app-config.service.spec.ts +++ b/lib/core/src/lib/app-config/app-config.service.spec.ts @@ -188,4 +188,14 @@ describe('AppConfigService', () => { expect(appConfigService.get('files.excluded')[0]).toBe('excluded'); }); + + it('should execute callback function if is passed to the load method', async () => { + const fakeCallBack = jasmine.createSpy('fakeCallBack'); + fakeCallBack.and.returnValue(()=>{}); + + await appConfigService.load(fakeCallBack); + + expect(fakeCallBack).toHaveBeenCalled(); + }); + }); diff --git a/lib/core/src/lib/app-config/app-config.service.ts b/lib/core/src/lib/app-config/app-config.service.ts index b272bcc50fc..314522a5add 100644 --- a/lib/core/src/lib/app-config/app-config.service.ts +++ b/lib/core/src/lib/app-config/app-config.service.ts @@ -18,13 +18,14 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ObjectUtils } from '../common/utils/object-utils'; -import { Observable, Subject } from 'rxjs'; +import { Observable, ReplaySubject } from 'rxjs'; import { map, distinctUntilChanged, take } from 'rxjs/operators'; import { ExtensionConfig, ExtensionService, mergeObjects } from '@alfresco/adf-extensions'; import { OpenidConfiguration } from '../auth/interfaces/openid-configuration.interface'; import { OauthConfigModel } from '../auth/models/oauth-config.model'; /* spellchecker: disable */ + // eslint-disable-next-line no-shadow export enum AppConfigValues { APP_CONFIG_LANGUAGES_KEY = 'languages', @@ -44,7 +45,9 @@ export enum AppConfigValues { AUTH_WITH_CREDENTIALS = 'auth.withCredentials', APPLICATION = 'application', STORAGE_PREFIX = 'application.storagePrefix', - NOTIFY_DURATION = 'notificationDefaultDuration' + NOTIFY_DURATION = 'notificationDefaultDuration', + CONTENT_TICKET_STORAGE_LABEL = 'ticket-ECM', + PROCESS_TICKET_STORAGE_LABEL = 'ticket-BPM' } // eslint-disable-next-line no-shadow @@ -71,11 +74,15 @@ export class AppConfigService { }; status: Status = Status.INIT; - protected onLoadSubject: Subject; + protected onLoadSubject: ReplaySubject; onLoad: Observable; + get isLoaded() { + return this.status === Status.LOADED; + } + constructor(protected http: HttpClient, protected extensionService: ExtensionService) { - this.onLoadSubject = new Subject(); + this.onLoadSubject = new ReplaySubject(); this.onLoad = this.onLoadSubject.asObservable(); extensionService.setup$.subscribe((config) => { @@ -92,7 +99,7 @@ export class AppConfigService { select(property: string): Observable { return this.onLoadSubject .pipe( - map((config) => config[property]), + map((config) => ObjectUtils.getValue(config, property)), distinctUntilChanged() ); } @@ -160,8 +167,7 @@ export class AppConfigService { this.onLoadSubject.next(this.config); } - protected onDataLoaded(data: any) { - this.config = Object.assign({}, this.config, data || {}); + protected onDataLoaded() { this.onLoadSubject.next(this.config); this.extensionService.setup$ @@ -182,9 +188,10 @@ export class AppConfigService { /** * Loads the config file. * + * @param callback an optional callback to execute when configuration is loaded * @returns Notification when loading is complete */ - load(): Promise { + load(callback?: (...args: any[]) => any): Promise { return new Promise((resolve) => { const configUrl = `app.config.json?v=${Date.now()}`; @@ -193,8 +200,10 @@ export class AppConfigService { this.http.get(configUrl).subscribe( (data: any) => { this.status = Status.LOADED; + this.config = Object.assign({}, this.config, data || {}); + callback?.(); resolve(data); - this.onDataLoaded(data); + this.onDataLoaded(); }, () => { // eslint-disable-next-line no-console @@ -227,6 +236,8 @@ export class AppConfigService { resolve(res); }, error: (err: any) => { + // eslint-disable-next-line no-console + console.error('hostIdp not correctly configured or unreachable'); reject(err); } }); @@ -262,4 +273,5 @@ export class AppConfigService { return result; } + } diff --git a/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.spec.ts b/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.spec.ts index 29145608a78..d2f252eb512 100644 --- a/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.spec.ts +++ b/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.spec.ts @@ -17,9 +17,10 @@ import { HttpClient, HttpHandler, HttpRequest } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; -import { Observable, of } from 'rxjs'; +import { EMPTY, Observable, of } from 'rxjs'; import { AuthBearerInterceptor } from './auth-bearer.interceptor'; import { AuthenticationService } from '../services/authentication.service'; +import { RedirectAuthService } from '../oidc/redirect-auth.service'; const mockNext: HttpHandler = { handle: () => new Observable(subscriber => { @@ -40,7 +41,8 @@ describe('AuthBearerInterceptor', () => { HttpClient, HttpHandler, AuthBearerInterceptor, - AuthenticationService + AuthenticationService, + { provide: RedirectAuthService, useValue: { onLogin: EMPTY } } ] }); @@ -85,7 +87,7 @@ describe('AuthBearerInterceptor', () => { }); it('should interceptor add auth token to every URL if excluded URLs array is empty', () => { - spyOn(authService, 'getBearerExcludedUrls').and.returnValue([]); + spyOnProperty(interceptor, 'bearerExcludedUrls').and.returnValue([]); const mockUrls = [ 'http://example.com/auth/realms/testpath', diff --git a/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts b/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts index afb8c0b4bef..e89309b8079 100644 --- a/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts +++ b/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts @@ -16,40 +16,37 @@ */ import { throwError as observableThrowError, Observable } from 'rxjs'; -import { Injectable, Injector } from '@angular/core'; +import { Injectable } from '@angular/core'; import { HttpHandler, HttpInterceptor, HttpRequest, HttpSentEvent, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent, HttpHeaders } from '@angular/common/http'; -import { AuthenticationService } from '../services/authentication.service'; import { catchError, mergeMap } from 'rxjs/operators'; +import { AuthenticationService } from '../services/authentication.service'; @Injectable() export class AuthBearerInterceptor implements HttpInterceptor { - private excludedUrlsRegex: RegExp[]; + private _bearerExcludedUrls: readonly string[] = ['resources/', 'assets/', 'auth/realms', 'idp/']; - constructor(private injector: Injector, private authService: AuthenticationService) { } + private excludedUrlsRegex: RegExp[]; + + constructor(private authenticationService: AuthenticationService) { } private loadExcludedUrlsRegex() { - const excludedUrls = this.authService.getBearerExcludedUrls(); + const excludedUrls = this.bearerExcludedUrls; this.excludedUrlsRegex = excludedUrls.map((urlPattern) => new RegExp(`^https?://[^/]+/${urlPattern}`, 'i')) || []; } intercept(req: HttpRequest, next: HttpHandler): Observable | HttpUserEvent> { - this.authService = this.injector.get(AuthenticationService); - - if (!this.authService?.getBearerExcludedUrls()) { - return next.handle(req); - } if (!this.excludedUrlsRegex) { this.loadExcludedUrlsRegex(); } - const urlRequest = req.url; - const shallPass: boolean = this.excludedUrlsRegex.some((regex) => regex.test(urlRequest)); + const requestUrl = req.url; + const shallPass: boolean = this.excludedUrlsRegex.some((regex) => regex.test(requestUrl)); if (shallPass) { return next.handle(req) .pipe( @@ -57,10 +54,10 @@ export class AuthBearerInterceptor implements HttpInterceptor { ); } - return this.authService.addTokenToHeader(req.headers) + return this.authenticationService.addTokenToHeader(requestUrl, req.headers) .pipe( mergeMap((headersWithBearer) => { - const headerWithContentType = this.appendJsonContentType(headersWithBearer); + const headerWithContentType = this.appendJsonContentType(headersWithBearer, req.body); const kcReq = req.clone({ headers: headerWithContentType}); return next.handle(kcReq) .pipe( @@ -70,7 +67,7 @@ export class AuthBearerInterceptor implements HttpInterceptor { ); } - private appendJsonContentType(headers: HttpHeaders): HttpHeaders { + private appendJsonContentType(headers: HttpHeaders, reqBody: any): HttpHeaders { // prevent adding any content type, to properly handle formData with boundary browser generated value, // as adding any Content-Type its going to break the upload functionality @@ -79,11 +76,15 @@ export class AuthBearerInterceptor implements HttpInterceptor { return headers.delete('Content-Type'); } - if (!headers.get('Content-Type')) { + if (!headers.get('Content-Type') && !(reqBody instanceof FormData)) { return headers.set('Content-Type', 'application/json;charset=UTF-8'); } return headers; } + protected get bearerExcludedUrls(): readonly string[] { + return this._bearerExcludedUrls; + } + } diff --git a/lib/core/src/lib/auth/basic-auth/basic-alfresco-auth.service.ts b/lib/core/src/lib/auth/basic-auth/basic-alfresco-auth.service.ts new file mode 100644 index 00000000000..0808fa31e00 --- /dev/null +++ b/lib/core/src/lib/auth/basic-auth/basic-alfresco-auth.service.ts @@ -0,0 +1,374 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; +import { Authentication } from '../interfaces/authentication.interface'; +import { CookieService } from '../../common/services/cookie.service'; +import { ContentAuth } from './content-auth'; +import { ProcessAuth } from './process-auth'; +import { catchError, map } from 'rxjs/operators'; +import { from, Observable } from 'rxjs'; +import { RedirectionModel } from '../models/redirection.model'; +import { BaseAuthenticationService } from '../services/base-authentication.service'; +import { LogService } from '../../common'; +import { HttpHeaders } from '@angular/common/http'; + +const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME'; +const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30; + +@Injectable({ + providedIn: 'root' +}) +export class BasicAlfrescoAuthService extends BaseAuthenticationService { + + protected redirectUrl: RedirectionModel = null; + + authentications: Authentication = { + basicAuth: { + ticket: '' + }, + type: 'basic' + }; + + constructor( + logService: LogService, + appConfig: AppConfigService, + cookie: CookieService, + private contentAuth: ContentAuth, + private processAuth: ProcessAuth + ) { + super(appConfig, cookie, logService); + + this.appConfig.onLoad + .subscribe(() => { + if (!this.isOauth() && this.isLoggedIn()) { + this.onLogin.next('logged-in'); + } + }); + + this.contentAuth.onLogout.pipe(map((event) => { + this.onLogout.next(event); + })); + this.contentAuth.onLogin.pipe(map((event) => { + this.onLogin.next(event); + })); + this.contentAuth.onError.pipe(map((event) => { + this.onError.next(event); + })); + this.processAuth.onLogout.pipe(map((event) => { + this.onLogout.next(event); + })); + this.processAuth.onLogin.pipe(map((event) => { + this.onLogin.next(event); + })); + this.processAuth.onError.pipe(map((event) => { + this.onError.next(event); + })); + } + + /** + * Logs the user in. + * + * @param username Username for the login + * @param password Password for the login + * @param rememberMe Stores the user's login details if true + * @returns Object with auth type ("ECM", "BPM" or "ALL") and auth ticket + */ + login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string; ticket: any }> { + return from(this.executeLogin(username, password)).pipe( + map((response: any) => { + this.saveRememberMeCookie(rememberMe); + this.onLogin.next(response); + return { + type: this.appConfig.get(AppConfigValues.PROVIDERS), + ticket: response + }; + }), + catchError((err) => this.handleError(err)) + ); + } + + /** + * login Alfresco API + * + * @param username username to login + * @param password password to login + * @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected. + */ + async executeLogin(username: string, password: string): Promise { + if (!this.isCredentialValid(username) || !this.isCredentialValid(password)) { + return Promise.reject(new Error('missing username or password')); + } + + if (username) { + username = username.trim(); + } + + if (this.isBPMProvider()) { + try { + return await this.processAuth.login(username, password); + } catch (e) { + return Promise.reject(e); + } + + } else if (this.isECMProvider()) { + try { + return await this.contentAuth.login(username, password); + } catch (e) { + return Promise.reject(e); + } + + } else if (this.isALLProvider()) { + return this.loginBPMECM(username, password); + } else { + return Promise.reject(new Error('Unknown configuration')); + } + + } + + private loginBPMECM(username: string, password: string): Promise { + const contentPromise = this.contentAuth.login(username, password); + const processPromise = this.processAuth.login(username, password); + + return new Promise((resolve, reject) => { + Promise.all([contentPromise, processPromise]).then( + (data) => { + this.onLogin.next('success'); + resolve(data); + }, + (error) => { + this.contentAuth.invalidateSession(); + this.processAuth.invalidateSession(); + + if (error.status === 401) { + this.onError.next('unauthorized'); + } + this.onError.next('error'); + reject(error); + }); + }); + } + + /** + * Checks whether the "remember me" cookie was set or not. + * + * @returns True if set, false otherwise + */ + isRememberMeSet(): boolean { + return this.cookie.getItem(REMEMBER_ME_COOKIE_KEY) !== null; + } + + /** + * Saves the "remember me" cookie as either a long-life cookie or a session cookie. + * + * @param rememberMe Enables a long-life cookie + */ + saveRememberMeCookie(rememberMe: boolean): void { + let expiration = null; + + if (rememberMe) { + expiration = new Date(); + const time = expiration.getTime(); + const expireTime = time + REMEMBER_ME_UNTIL; + expiration.setTime(expireTime); + } + this.cookie.setItem(REMEMBER_ME_COOKIE_KEY, '1', expiration, null); + } + + isCredentialValid(credential: string): boolean { + return credential !== undefined && credential !== null && credential !== ''; + } + + getToken(): string { + if (this.isBPMProvider()) { + return this.processAuth.getToken(); + } else if (this.isECMProvider()) { + return this.contentAuth.getToken(); + } else if (this.isALLProvider()) { + return this.contentAuth.getToken(); + } else { + return ''; + } + } + + /** + * @deprecated + * @returns content auth token + */ + getTicketEcm(): string { + return this.contentAuth.getToken(); + } + + /** + * @deprecated + * @returns process auth token + */ + getTicketBpm(): string { + return this.processAuth.getToken(); + } + + isBpmLoggedIn(): boolean { + return this.processAuth.isLoggedIn(); + } + + isEcmLoggedIn(): boolean { + return this.contentAuth.isLoggedIn(); + } + + isLoggedIn(): boolean { + const authWithCredentials = this.isKerberosEnabled(); + + if (this.isBPMProvider()) { + return this.processAuth.isLoggedIn(); + } else if (this.isECMProvider()) { + return authWithCredentials ? true : this.contentAuth.isLoggedIn(); + } else if (this.isALLProvider()) { + return authWithCredentials ? true : (this.contentAuth.isLoggedIn() && this.processAuth.isLoggedIn()); + } else { + return false; + } + } + + /** + * logout Alfresco API + */ + async logout(): Promise { + if (this.isBPMProvider()) { + return this.processAuth.logout(); + } else if (this.isECMProvider()) { + return this.contentAuth.logout(); + } else if (this.isALLProvider()) { + return this.logoutBPMECM(); + } + return Promise.resolve(); + } + + private logoutBPMECM(): Promise { + const contentPromise = this.contentAuth.logout(); + const processPromise = this.processAuth.logout(); + + return new Promise((resolve, reject) => { + Promise.all([contentPromise, processPromise]).then( + () => { + this.contentAuth.ticket = undefined; + this.processAuth.ticket = undefined; + this.onLogout.next('logout'); + resolve('logout'); + }, + (error) => { + if (error.status === 401) { + this.onError.next('unauthorized'); + } + this.onError.next('error'); + reject(error); + }); + }); + + } + + reset(): void { + } + + /** + * Gets the URL to redirect to after login. + * + * @returns The redirect URL + */ + getRedirect(): string { + const provider = this.appConfig.get(AppConfigValues.PROVIDERS); + return this.hasValidRedirection(provider) ? this.redirectUrl.url : null; + } + + setRedirect(url?: RedirectionModel) { + this.redirectUrl = url; + } + + private hasValidRedirection(provider: string): boolean { + return this.redirectUrl && (this.redirectUrl.provider === provider || this.hasSelectedProviderAll(provider)); + } + + private hasSelectedProviderAll(provider: string): boolean { + return this.redirectUrl && (this.redirectUrl.provider === 'ALL' || provider === 'ALL'); + } + + getBpmUsername(): string { + return this.processAuth.getUsername(); + } + + getEcmUsername(): string { + return this.contentAuth.getUsername(); + } + + getUsername(): string { + if (this.isBPMProvider()) { + return this.processAuth.getUsername(); + } else if (this.isECMProvider()) { + return this.contentAuth.getUsername(); + } else { + return this.contentAuth.getUsername(); + } + } + + /** + * Does kerberos enabled? + * + * @returns True if enabled, false otherwise + */ + isKerberosEnabled(): boolean { + return this.appConfig.get(AppConfigValues.AUTH_WITH_CREDENTIALS, false); + } + + getAuthHeaders(requestUrl: string, header: HttpHeaders): HttpHeaders { + return this.addBasicAuth(requestUrl, header); + } + + private addBasicAuth(requestUrl: string, header: HttpHeaders): HttpHeaders { + const ticket = this.getTicketEcmBase64(requestUrl); + + if (!ticket) { + return header; + } + + return header.set('Authorization', ticket); + } + + async requireAlfTicket(): Promise { + return this.contentAuth.requireAlfTicket(); + } + + /** + * Gets the BPM ticket from the Storage in Base 64 format. + * + * @param requestUrl the request url + * @returns The ticket or `null` if none was found + */ + private getTicketEcmBase64(requestUrl: string): string | null { + let ticket = null; + + const contextRootBpm = this.appConfig.get(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app'; + const contextRoot = this.appConfig.get(AppConfigValues.CONTEXTROOTECM) || 'alfresco'; + + if (contextRoot && requestUrl.indexOf(contextRoot) !== -1) { + ticket = 'Basic ' + btoa(this.contentAuth.getToken()); + } else if (contextRootBpm && requestUrl.indexOf(contextRootBpm) !== -1) { + ticket = 'Basic ' + this.processAuth.getToken(); + } + + return ticket; + } +} diff --git a/lib/core/src/lib/auth/basic-auth/content-auth.ts b/lib/core/src/lib/auth/basic-auth/content-auth.ts new file mode 100644 index 00000000000..c7918316b0b --- /dev/null +++ b/lib/core/src/lib/auth/basic-auth/content-auth.ts @@ -0,0 +1,220 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { AdfHttpClient } from '@alfresco/adf-core/api'; +import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; +import { StorageService } from '../../common/services/storage.service'; +import { ReplaySubject, Subject } from 'rxjs'; +import { Authentication } from '../interfaces/authentication.interface'; + +export interface TicketBody { + userId?: string; + password?: string; +} + +export interface TicketEntry { + entry: { + id?: string; + userId?: string; + }; +} + +@Injectable({ + providedIn: 'root' +}) +export class ContentAuth { + + onLogin = new ReplaySubject(1); + onLogout = new ReplaySubject(1); + onError = new Subject(); + + ticket: string; + config = { + ticketEcm: null + }; + + authentications: Authentication = { + basicAuth: { + ticket: '' + }, + type: 'basic' + }; + + get basePath(): string { + const contextRootEcm = this.appConfigService.get(AppConfigValues.CONTEXTROOTECM) || 'alfresco'; + return this.appConfigService.get(AppConfigValues.ECMHOST) + '/' + contextRootEcm + '/api/-default-/public/authentication/versions/1'; + } + + constructor(private appConfigService: AppConfigService, + private adfHttpClient: AdfHttpClient, + private storageService: StorageService) { + this.appConfigService.onLoad.subscribe(() => { + this.setConfig(); + }); + } + + private setConfig() { + if (this.storageService.getItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL)) { + this.setTicket(this.storageService.getItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL)); + } + + } + + saveUsername(username: string) { + this.storageService.setItem('ACS_USERNAME', username); + } + + getUsername() { + return this.storageService.getItem('ACS_USERNAME'); + } + + /** + * login Alfresco API + * + * @param username username to login + * @param password password to login + * @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected. + */ + login(username: string, password: string): Promise { + this.authentications.basicAuth.username = username; + this.authentications.basicAuth.password = password; + + const loginRequest: any = {}; + + loginRequest.userId = this.authentications.basicAuth.username; + loginRequest.password = this.authentications.basicAuth.password; + + return new Promise((resolve, reject) => { + this.createTicket(loginRequest) + .then((data: any) => { + this.saveUsername(username); + this.setTicket(data.entry.id); + this.adfHttpClient.emit('success'); + this.onLogin.next('success'); + resolve(data.entry.id); + }) + .catch((error) => { + this.saveUsername(''); + if (error.status === 401) { + this.adfHttpClient.emit('unauthorized', error); + this.onError.next('unauthorized'); + } else if (error.status === 403) { + this.adfHttpClient.emit('forbidden', error); + this.onError.next('forbidden'); + } else { + this.adfHttpClient.emit('error', error); + this.onError.next('error'); + } + reject(error); + }); + }); + } + + /** + * logout Alfresco API + * + * @returns A promise that returns { authentication ticket} if resolved and {error} if rejected. + */ + logout(): Promise { + this.saveUsername(''); + return new Promise((resolve, reject) => { + this.deleteTicket().then( + () => { + this.invalidateSession(); + this.adfHttpClient.emit('logout'); + this.onLogout.next('logout'); + resolve('logout'); + }, + (error) => { + if (error.status === 401) { + this.adfHttpClient.emit('unauthorized'); + this.onError.next('unauthorized'); + } + this.adfHttpClient.emit('error'); + this.onError.next('error'); + reject(error); + }); + }); + } + + /** + * Set the current Ticket + * + * @param ticket a string representing the ticket + */ + setTicket(ticket: string) { + this.authentications.basicAuth.username = 'ROLE_TICKET'; + this.authentications.basicAuth.password = ticket; + this.config.ticketEcm = ticket; + this.storageService.setItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL, ticket); + this.ticket = ticket; + } + + /** + * @returns the current Ticket + */ + getToken(): string { + if (!this.ticket) { + this.onError.next('error'); + } + + return this.ticket; + } + + invalidateSession() { + this.storageService.removeItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL); + this.authentications.basicAuth.username = null; + this.authentications.basicAuth.password = null; + this.config.ticketEcm = null; + this.ticket = null; + } + + /** + * @returns If the client is logged in return true + */ + isLoggedIn(): boolean { + return !!this.ticket; + } + + /** + * @returns return the Authentication + */ + getAuthentication() { + return this.authentications; + } + + createTicket(ticketBodyCreate: TicketBody): Promise { + if (ticketBodyCreate === null || ticketBodyCreate === undefined) { + this.onError.next((`Missing param ticketBodyCreate`)); + + throw new Error(`Missing param ticketBodyCreate`); + } + + return this.adfHttpClient.post(this.basePath + '/tickets', {bodyParam: ticketBodyCreate}); + } + + async requireAlfTicket(): Promise { + const ticket = await this.adfHttpClient.get(this.basePath + '/tickets/-me-'); + this.setTicket(ticket.entry.id); + } + + deleteTicket(): Promise { + return this.adfHttpClient.delete(this.basePath + '/tickets/-me-'); + } + +} diff --git a/lib/core/src/lib/auth/basic-auth/process-auth.ts b/lib/core/src/lib/auth/basic-auth/process-auth.ts new file mode 100644 index 00000000000..7374a160d91 --- /dev/null +++ b/lib/core/src/lib/auth/basic-auth/process-auth.ts @@ -0,0 +1,210 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { AdfHttpClient } from '@alfresco/adf-core/api'; +import { Authentication } from '../interfaces/authentication.interface'; +import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; +import { StorageService } from '../../common/services/storage.service'; +import { ReplaySubject, Subject } from 'rxjs'; + + +@Injectable({ + providedIn: 'root' +}) +export class ProcessAuth { + + onLogin = new ReplaySubject(1); + onLogout = new ReplaySubject(1); + onError = new Subject(); + + ticket: string; + config = { + ticketBpm: null + }; + + authentications: Authentication = { + basicAuth: {ticket: ''}, type: 'activiti' + }; + + get basePath(): string { + const contextRootBpm = this.appConfigService.get(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app'; + return this.appConfigService.get(AppConfigValues.BPMHOST) + '/' + contextRootBpm; + } + + constructor(private appConfigService: AppConfigService, + private adfHttpClient: AdfHttpClient, + private storageService: StorageService) { + this.appConfigService.onLoad.subscribe(() => { + this.setConfig(); + }); + } + + private setConfig() { + this.ticket = undefined; + + this.setTicket(this.storageService.getItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL)); + } + + saveUsername(username: string) { + this.storageService.setItem('APS_USERNAME', username); + } + + getUsername() { + return this.storageService.getItem('APS_USERNAME'); + } + + /** + * login Activiti API + * + * @param username Username to login + * @param password Password to login + * @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected. + */ + login(username: string, password: string): Promise { + this.authentications.basicAuth.username = username; + this.authentications.basicAuth.password = password; + + const options = { + headerParams: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cache-Control': 'no-cache' + }, + formParams: { + j_username: this.authentications.basicAuth.username, + j_password: this.authentications.basicAuth.password, + _spring_security_remember_me: true, + submit: 'Login' + }, + contentType: 'application/x-www-form-urlencoded', + accept: 'application/json' + }; + + const promise: any = new Promise((resolve, reject) => { + this.adfHttpClient.post(this.basePath + '/app/authentication', options).then( + () => { + this.saveUsername(username); + const ticket = this.basicAuth(this.authentications.basicAuth.username, this.authentications.basicAuth.password); + this.setTicket(ticket); + this.onLogin.next('success'); + this.adfHttpClient.emit('success'); + this.adfHttpClient.emit('logged-in'); + resolve(ticket); + }, + (error) => { + this.saveUsername(''); + if (error.status === 401) { + this.adfHttpClient.emit('unauthorized', error); + this.onError.next('unauthorized'); + } else if (error.status === 403) { + this.adfHttpClient.emit('forbidden', error); + this.onError.next('forbidden'); + } else { + this.adfHttpClient.emit('error', error); + this.onError.next('error'); + } + reject(error); + }); + }); + + return promise; + } + + /** + * logout Alfresco API + * + * @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected. + */ + async logout(): Promise { + this.saveUsername(''); + return new Promise((resolve, reject) => { + this.adfHttpClient.get(this.basePath + `/app/logout`, {}).then( + () => { + this.invalidateSession(); + this.onLogout.next('logout'); + this.adfHttpClient.emit('logout'); + resolve('logout'); + }, + (error) => { + if (error.status === 401) { + this.adfHttpClient.emit('unauthorized'); + this.onError.next('unauthorized'); + } + this.adfHttpClient.emit('error'); + this.onError.next('error'); + reject(error); + }); + }); + } + + basicAuth(username: string, password: string): string { + const str: any = username + ':' + password; + + let base64; + + if (typeof Buffer === 'function') { + base64 = Buffer.from(str.toString(), 'binary').toString('base64'); + } else { + base64 = btoa(str); + } + + return `Basic ${base64}`; + } + + /** + * Set the current Ticket + * + * @param ticket a string representing the ticket + */ + setTicket(ticket: string) { + if (ticket && ticket !== 'null') { + this.authentications.basicAuth.ticket = ticket; + this.authentications.basicAuth.password = null; + this.config.ticketBpm = ticket; + this.storageService.setItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL, ticket); + this.ticket = ticket; + } + } + + invalidateSession() { + this.storageService.removeItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL); + this.authentications.basicAuth.ticket = null; + this.authentications.basicAuth.password = null; + this.authentications.basicAuth.username = null; + this.config.ticketBpm = null; + this.ticket = null; + } + + /** + * @returns the current Ticket + */ + getToken(): string { + if (!this.ticket) { + this.onError.next('error'); + return null; + } + + return this.ticket; + } + + /** + * @returns If the client is logged in return true + */ + isLoggedIn(): boolean { + return !!this.ticket; + } +} diff --git a/lib/core/src/lib/auth/guard/auth-guard-base.ts b/lib/core/src/lib/auth/guard/auth-guard-base.ts index b91bc0fbfdf..036ed6386ed 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-base.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-base.ts @@ -17,24 +17,35 @@ import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, UrlTree } from '@angular/router'; import { AuthenticationService } from '../services/authentication.service'; -import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; +import { + AppConfigService, + AppConfigValues +} from '../../app-config/app-config.service'; import { OauthConfigModel } from '../models/oauth-config.model'; import { MatDialog } from '@angular/material/dialog'; import { StorageService } from '../../common/services/storage.service'; import { Observable } from 'rxjs'; -import { inject } from '@angular/core'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../services/oidc-authentication.service'; + export abstract class AuthGuardBase implements CanActivate, CanActivateChild { - protected authenticationService = inject(AuthenticationService); - protected router = inject(Router); - protected appConfigService = inject(AppConfigService); - protected dialog = inject(MatDialog); - private storageService = inject(StorageService); protected get withCredentials(): boolean { return this.appConfigService.get('auth.withCredentials', false); } + constructor( + protected authenticationService: AuthenticationService, + protected basicAlfrescoAuthService: BasicAlfrescoAuthService, + protected oidcAuthenticationService: OidcAuthenticationService, + protected router: Router, + protected appConfigService: AppConfigService, + protected dialog: MatDialog, + private storageService: StorageService + ) { + } + abstract checkLogin( activeRoute: ActivatedRouteSnapshot, redirectUrl: string @@ -78,15 +89,17 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild { let urlToRedirect = `/${this.getLoginRoute()}`; if (!this.authenticationService.isOauth()) { - this.authenticationService.setRedirect({ + this.basicAlfrescoAuthService.setRedirect({ provider: this.getProvider(), url }); urlToRedirect = `${urlToRedirect}?redirectUrl=${url}`; return this.navigate(urlToRedirect); - } else if (this.getOauthConfig().silentLogin && !this.authenticationService.isPublicUrl()) { - this.authenticationService.ssoImplicitLogin(); + } else if (this.getOauthConfig().silentLogin && !this.oidcAuthenticationService.isPublicUrl()) { + if (!this.oidcAuthenticationService.hasValidIdToken() || !this.oidcAuthenticationService.hasValidAccessToken()) { + this.oidcAuthenticationService.ssoImplicitLogin(); + } } else { return this.navigate(urlToRedirect); } @@ -101,7 +114,13 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild { } protected getOauthConfig(): OauthConfigModel { - return this.appConfigService.oauth2; + return ( + this.appConfigService && + this.appConfigService.get( + AppConfigValues.OAUTHCONFIG, + null + ) + ); } protected getLoginRoute(): string { @@ -113,12 +132,21 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild { } protected isOAuthWithoutSilentLogin(): boolean { - const oauth = this.appConfigService.oauth2; - return this.authenticationService.isOauth() && !!oauth && !oauth.silentLogin; + const oauth = this.appConfigService.get( + AppConfigValues.OAUTHCONFIG, + null + ); + return ( + this.authenticationService.isOauth() && !!oauth && !oauth.silentLogin + ); } protected isSilentLogin(): boolean { - const oauth = this.appConfigService.oauth2; + const oauth = this.appConfigService.get( + AppConfigValues.OAUTHCONFIG, + null + ); + return this.authenticationService.isOauth() && oauth && oauth.silentLogin; } } diff --git a/lib/core/src/lib/auth/guard/auth-guard-bpm.service.spec.ts b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.spec.ts index 40288684c24..14619d26c5c 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-bpm.service.spec.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.spec.ts @@ -23,11 +23,16 @@ import { RouterStateSnapshot, Router } from '@angular/router'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { MatDialog } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../services/oidc-authentication.service'; describe('AuthGuardService BPM', () => { let authGuard: AuthGuardBpm; let authService: AuthenticationService; + let basicAlfrescoAuthService: BasicAlfrescoAuthService; + let oidcAuthenticationService: OidcAuthenticationService; + let router: Router; let appConfigService: AppConfigService; @@ -36,9 +41,21 @@ describe('AuthGuardService BPM', () => { imports: [ TranslateModule.forRoot(), CoreTestingModule + ], + providers: [ + { + provide: OidcAuthenticationService, useValue: { + ssoImplicitLogin: () => { }, + isPublicUrl: () => false, + hasValidIdToken: () => false, + isLoggedIn: () => false + } + } ] }); localStorage.clear(); + basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); + oidcAuthenticationService = TestBed.inject(OidcAuthenticationService); authService = TestBed.inject(AuthenticationService); authGuard = TestBed.inject(AuthGuardBpm); router = TestBed.inject(Router); @@ -53,8 +70,8 @@ describe('AuthGuardService BPM', () => { spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); - spyOn(authService, 'isPublicUrl').and.returnValue(false); - spyOn(authService, 'ssoImplicitLogin').and.stub(); + spyOn(oidcAuthenticationService, 'isPublicUrl').and.returnValue(false); + spyOn(oidcAuthenticationService, 'ssoImplicitLogin').and.stub(); appConfigService.config.oauth2 = { silentLogin: true, @@ -69,7 +86,7 @@ describe('AuthGuardService BPM', () => { const route = { url: 'abc' } as RouterStateSnapshot; expect(await authGuard.canActivate(null, route)).toBeFalsy(); - expect(authService.ssoImplicitLogin).toHaveBeenCalledTimes(1); + expect(oidcAuthenticationService.ssoImplicitLogin).toHaveBeenCalledTimes(1); }); it('if the alfresco js api is logged in should canActivate be true', async () => { @@ -130,53 +147,53 @@ describe('AuthGuardService BPM', () => { }); it('should set redirect url', () => { - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', url: 'some-url' }); - expect(authService.getRedirect()).toEqual('some-url'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url'); }); it('should set redirect navigation commands with query params', () => { - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url;q=123' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', url: 'some-url;q=123' }); - expect(authService.getRedirect()).toEqual('some-url;q=123'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url;q=123'); }); it('should set redirect navigation commands with query params', () => { - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: '/' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', url: '/' }); - expect(authService.getRedirect()).toEqual('/'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('/'); }); it('should get redirect url from config if there is one configured', () => { appConfigService.config.loginRoute = 'fakeLoginRoute'; - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', url: 'some-url' }); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url')); @@ -187,13 +204,13 @@ describe('AuthGuardService BPM', () => { spyOn(materialDialog, 'closeAll'); - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', url: 'some-url' }); diff --git a/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts index 4d511832a95..59a1a3457bf 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts @@ -16,13 +16,30 @@ */ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; +import { AppConfigService } from '../../app-config/app-config.service'; +import { AuthenticationService } from '../services/authentication.service'; import { AuthGuardBase } from './auth-guard-base'; +import { MatDialog } from '@angular/material/dialog'; +import { StorageService } from '../../common/services/storage.service'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../services/oidc-authentication.service'; @Injectable({ providedIn: 'root' }) export class AuthGuardBpm extends AuthGuardBase { + + constructor(authenticationService: AuthenticationService, + basicAlfrescoAuthService: BasicAlfrescoAuthService, + oidcAuthenticationService: OidcAuthenticationService, + router: Router, + appConfigService: AppConfigService, + dialog: MatDialog, + storageService: StorageService) { + super(authenticationService,basicAlfrescoAuthService, oidcAuthenticationService,router, appConfigService, dialog, storageService); + } + async checkLogin(_: ActivatedRouteSnapshot, redirectUrl: string): Promise { if (this.authenticationService.isBpmLoggedIn() || this.withCredentials) { return true; diff --git a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts index 70856196faf..d4c26591db3 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts @@ -23,11 +23,15 @@ import { RouterStateSnapshot, Router } from '@angular/router'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { MatDialog } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; +import { OidcAuthenticationService } from '../services/oidc-authentication.service'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; describe('AuthGuardService ECM', () => { let authGuard: AuthGuardEcm; let authService: AuthenticationService; + let basicAlfrescoAuthService: BasicAlfrescoAuthService; + let oidcAuthenticationService: OidcAuthenticationService; let router: Router; let appConfigService: AppConfigService; @@ -36,9 +40,21 @@ describe('AuthGuardService ECM', () => { imports: [ TranslateModule.forRoot(), CoreTestingModule + ], + providers: [ + { + provide: OidcAuthenticationService, useValue: { + ssoImplicitLogin: () => { }, + isPublicUrl: () => false, + hasValidIdToken: () => false, + isLoggedIn: () => false + } + } ] }); localStorage.clear(); + oidcAuthenticationService = TestBed.inject(OidcAuthenticationService); + basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); authService = TestBed.inject(AuthenticationService); authGuard = TestBed.inject(AuthGuardEcm); router = TestBed.inject(Router); @@ -98,8 +114,8 @@ describe('AuthGuardService ECM', () => { it('should redirect url if the alfresco js api is NOT logged in and isOAuth with silentLogin', async () => { spyOn(authService, 'isEcmLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); - spyOn(authService, 'isPublicUrl').and.returnValue(false); - spyOn(authService, 'ssoImplicitLogin').and.stub(); + spyOn(oidcAuthenticationService, 'isPublicUrl').and.returnValue(false); + spyOn(oidcAuthenticationService, 'ssoImplicitLogin').and.stub(); appConfigService.config.oauth2 = { silentLogin: true, @@ -113,7 +129,7 @@ describe('AuthGuardService ECM', () => { const route = {url : 'abc'} as RouterStateSnapshot; expect(await authGuard.canActivate(null, route)).toBeFalsy(); - expect(authService.ssoImplicitLogin).toHaveBeenCalledTimes(1); + expect(oidcAuthenticationService.ssoImplicitLogin).toHaveBeenCalledTimes(1); }); it('should not redirect url if NOT logged in and isOAuth but no silentLogin configured', async () => { @@ -128,53 +144,53 @@ describe('AuthGuardService ECM', () => { }); it('should set redirect navigation commands', () => { - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', url: 'some-url' }); - expect(authService.getRedirect()).toEqual('some-url'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url'); }); it('should set redirect navigation commands with query params', () => { - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url;q=123' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', url: 'some-url;q=123' }); - expect(authService.getRedirect()).toEqual('some-url;q=123'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url;q=123'); }); it('should set redirect navigation commands with query params', () => { - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: '/' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', url: '/' }); - expect(authService.getRedirect()).toEqual('/'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('/'); }); it('should get redirect url from config if there is one configured', () => { appConfigService.config.loginRoute = 'fakeLoginRoute'; - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', url: 'some-url' }); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url')); @@ -185,13 +201,13 @@ describe('AuthGuardService ECM', () => { spyOn(materialDialog, 'closeAll'); - spyOn(authService, 'setRedirect').and.callThrough(); + spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); spyOn(router, 'navigateByUrl').and.stub(); const route = { url: 'some-url' } as RouterStateSnapshot; authGuard.canActivate(null, route); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', url: 'some-url' }); diff --git a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts index 0154b91ec7c..0621b528e17 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts @@ -16,13 +16,33 @@ */ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; +import { + ActivatedRouteSnapshot, Router, UrlTree +} from '@angular/router'; +import { AuthenticationService } from '../services/authentication.service'; +import { AppConfigService } from '../../app-config/app-config.service'; import { AuthGuardBase } from './auth-guard-base'; +import { MatDialog } from '@angular/material/dialog'; +import { StorageService } from '../../common/services/storage.service'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../services/oidc-authentication.service'; + @Injectable({ providedIn: 'root' }) export class AuthGuardEcm extends AuthGuardBase { + + constructor(authenticationService: AuthenticationService, + basicAlfrescoAuthService: BasicAlfrescoAuthService, + oidcAuthenticationService: OidcAuthenticationService, + router: Router, + appConfigService: AppConfigService, + dialog: MatDialog, + storageService: StorageService) { + super(authenticationService, basicAlfrescoAuthService, oidcAuthenticationService, router, appConfigService, dialog, storageService); + } + async checkLogin(_: ActivatedRouteSnapshot, redirectUrl: string): Promise { if (this.authenticationService.isEcmLoggedIn() || this.withCredentials) { return true; diff --git a/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts b/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts index 54e1d50d4d9..96ef8d0eb6a 100644 --- a/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts +++ b/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts @@ -23,6 +23,8 @@ import { AuthenticationService } from '../services/authentication.service'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; import { StorageService } from '../../common/services/storage.service'; +import { OidcAuthenticationService } from '../services/oidc-authentication.service'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; describe('AuthGuardService', () => { let state; @@ -31,17 +33,30 @@ describe('AuthGuardService', () => { let authGuard: AuthGuard; let storageService: StorageService; let appConfigService: AppConfigService; + let basicAlfrescoAuthService: BasicAlfrescoAuthService; + let oidcAuthenticationService: OidcAuthenticationService; beforeEach(() => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), CoreTestingModule + ], + providers: [ + { + provide: OidcAuthenticationService, useValue: { + ssoImplicitLogin: () => { }, + isPublicUrl: () => false, + hasValidIdToken: () => false + } + } ] }); localStorage.clear(); state = { url: '' }; authService = TestBed.inject(AuthenticationService); + basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); + oidcAuthenticationService = TestBed.inject(OidcAuthenticationService); router = TestBed.inject(Router); authGuard = TestBed.inject(AuthGuard); appConfigService = TestBed.inject(AppConfigService); @@ -110,13 +125,13 @@ describe('AuthGuardService', () => { }); it('should NOT redirect url if the User is NOT logged in and isOAuth but with silentLogin configured', async () => { - spyOn(authService, 'ssoImplicitLogin').and.stub(); + spyOn(oidcAuthenticationService, 'ssoImplicitLogin').and.stub(); spyOn(authService, 'isLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = true; expect(await authGuard.canActivate(null, state)).toBeFalsy(); - expect(authService.ssoImplicitLogin).toHaveBeenCalledTimes(1); + expect(oidcAuthenticationService.ssoImplicitLogin).toHaveBeenCalledTimes(1); }); it('should set redirect url', async () => { @@ -124,11 +139,11 @@ describe('AuthGuardService', () => { appConfigService.config.loginRoute = 'login'; spyOn(router, 'navigateByUrl'); - spyOn(authService, 'setRedirect'); + spyOn(basicAlfrescoAuthService, 'setRedirect'); await authGuard.canActivate(null, state); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', url: 'some-url' }); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/login?redirectUrl=some-url')); @@ -140,11 +155,11 @@ describe('AuthGuardService', () => { appConfigService.config.provider = 'ALL'; spyOn(router, 'navigateByUrl'); - spyOn(authService, 'setRedirect'); + spyOn(basicAlfrescoAuthService, 'setRedirect'); await authGuard.canActivate(null, state); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', url: 'some-url;q=query' }); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/login?redirectUrl=some-url;q=query')); @@ -155,11 +170,11 @@ describe('AuthGuardService', () => { appConfigService.config.loginRoute = 'fakeLoginRoute'; spyOn(router, 'navigateByUrl'); - spyOn(authService, 'setRedirect'); + spyOn(basicAlfrescoAuthService, 'setRedirect'); await authGuard.canActivate(null, state); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', url: 'some-url' }); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url')); @@ -169,11 +184,11 @@ describe('AuthGuardService', () => { state.url = '/'; spyOn(router, 'navigateByUrl'); - spyOn(authService, 'setRedirect'); + spyOn(basicAlfrescoAuthService, 'setRedirect'); await authGuard.canActivate(null, state); - expect(authService.setRedirect).toHaveBeenCalledWith({ + expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', url: '/' }); }); diff --git a/lib/core/src/lib/auth/guard/auth-guard.service.ts b/lib/core/src/lib/auth/guard/auth-guard.service.ts index 9f6b1897d4f..23eb64f47a2 100644 --- a/lib/core/src/lib/auth/guard/auth-guard.service.ts +++ b/lib/core/src/lib/auth/guard/auth-guard.service.ts @@ -16,9 +16,16 @@ */ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; +import { AuthenticationService } from '../services/authentication.service'; +import { AppConfigService } from '../../app-config/app-config.service'; import { AuthGuardBase } from './auth-guard-base'; import { JwtHelperService } from '../services/jwt-helper.service'; +import { MatDialog } from '@angular/material/dialog'; +import { StorageService } from '../../common/services/storage.service'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../services/oidc-authentication.service'; + @Injectable({ providedIn: 'root' @@ -27,8 +34,15 @@ export class AuthGuard extends AuthGuardBase { ticketChangeBind: any; - constructor(private jwtHelperService: JwtHelperService) { - super(); + constructor(private jwtHelperService: JwtHelperService, + authenticationService: AuthenticationService, + basicAlfrescoAuthService: BasicAlfrescoAuthService, + oidcAuthenticationService: OidcAuthenticationService, + router: Router, + appConfigService: AppConfigService, + dialog: MatDialog, + storageService: StorageService) { + super(authenticationService, basicAlfrescoAuthService, oidcAuthenticationService, router, appConfigService, dialog, storageService); this.ticketChangeBind = this.ticketChange.bind(this); window.addEventListener('storage', this.ticketChangeBind); diff --git a/lib/core/src/lib/auth/interfaces/authentication-service.interface.ts b/lib/core/src/lib/auth/interfaces/authentication-service.interface.ts new file mode 100644 index 00000000000..2c9b9241298 --- /dev/null +++ b/lib/core/src/lib/auth/interfaces/authentication-service.interface.ts @@ -0,0 +1,60 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HttpHeaders } from '@angular/common/http'; +import ee from 'event-emitter'; +import { Observable } from 'rxjs'; + +export interface AuthenticationServiceInterface { + + onError: any; + onLogin: any; + onLogout: any; + + on: ee.EmitterMethod; + off: ee.EmitterMethod; + once: ee.EmitterMethod; + emit: (type: string, ...args: any[]) => void; + + getToken(): string; + + isLoggedIn(): boolean; + + isOauth(): boolean; + + logout(): any; + + isEcmLoggedIn(): boolean; + + isBpmLoggedIn(): boolean; + + isECMProvider(): boolean; + + isBPMProvider(): boolean; + + isALLProvider(): boolean; + + getEcmUsername(): string; + + getBpmUsername(): string; + + getAuthHeaders(requestUrl: string, header: HttpHeaders): HttpHeaders; + + addTokenToHeader(requestUrl: string, headersArg?: HttpHeaders): Observable; + + reset(): void; +} diff --git a/lib/core/api/src/lib/clients/index.ts b/lib/core/src/lib/auth/interfaces/authentication.interface.ts similarity index 76% rename from lib/core/api/src/lib/clients/index.ts rename to lib/core/src/lib/auth/interfaces/authentication.interface.ts index 0855f4f09a9..318fa3fe82a 100644 --- a/lib/core/api/src/lib/clients/index.ts +++ b/lib/core/src/lib/auth/interfaces/authentication.interface.ts @@ -15,6 +15,14 @@ * limitations under the License. */ -export * from './activiti/activiti-client.types'; -export * from './alfresco-js-clients.module'; -export * from './discovery/discovery-client.types'; +export interface Authentication { + basicAuth?: BasicAuth; + cookie?: string; + type?: string; +} + +export interface BasicAuth { + username?: string; + password?: string; + ticket?: string; +} diff --git a/lib/core/src/lib/auth/oidc/auth.module.ts b/lib/core/src/lib/auth/oidc/auth.module.ts index d0907f20b89..354edad04a7 100644 --- a/lib/core/src/lib/auth/oidc/auth.module.ts +++ b/lib/core/src/lib/auth/oidc/auth.module.ts @@ -19,17 +19,12 @@ import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; import { AuthConfig, AUTH_CONFIG, OAuthModule, OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; import { AlfrescoApiNoAuthService } from '../../api-factories/alfresco-api-no-auth.service'; import { AlfrescoApiService } from '../../services/alfresco-api.service'; -import { AuthGuardBpm } from '../guard/auth-guard-bpm.service'; -import { AuthGuardEcm } from '../guard/auth-guard-ecm.service'; -import { AuthGuard } from '../guard/auth-guard.service'; import { AuthenticationService } from '../services/authentication.service'; import { StorageService } from '../../common/services/storage.service'; import { AuthModuleConfig, AUTH_MODULE_CONFIG } from './auth-config'; import { authConfigFactory, AuthConfigService } from './auth-config.service'; import { AuthRoutingModule } from './auth-routing.module'; import { AuthService } from './auth.service'; -import { OidcAuthGuard } from './oidc-auth.guard'; -import { OIDCAuthenticationService } from './oidc-authentication.service'; import { RedirectAuthService } from './redirect-auth.service'; import { AuthenticationConfirmationComponent } from './view/authentication-confirmation/authentication-confirmation.component'; @@ -51,10 +46,10 @@ export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage, imports: [AuthRoutingModule, OAuthModule.forRoot()], providers: [ { provide: OAuthStorage, useExisting: StorageService }, - { provide: AuthGuard, useClass: OidcAuthGuard }, - { provide: AuthGuardEcm, useClass: OidcAuthGuard }, - { provide: AuthGuardBpm, useClass: OidcAuthGuard }, - { provide: AuthenticationService, useClass: OIDCAuthenticationService }, + // { provide: AuthGuard, useClass: OidcAuthGuard }, + // { provide: AuthGuardEcm, useClass: OidcAuthGuard }, + // { provide: AuthGuardBpm, useClass: OidcAuthGuard }, + { provide: AuthenticationService}, { provide: AlfrescoApiService, useClass: AlfrescoApiNoAuthService }, { provide: AUTH_CONFIG, diff --git a/lib/core/src/lib/auth/oidc/auth.service.ts b/lib/core/src/lib/auth/oidc/auth.service.ts index 4c3be8fc7f7..be027665aba 100644 --- a/lib/core/src/lib/auth/oidc/auth.service.ts +++ b/lib/core/src/lib/auth/oidc/auth.service.ts @@ -22,6 +22,8 @@ import { Observable } from 'rxjs'; * Provide authentication/authorization through OAuth2/OIDC protocol. */ export abstract class AuthService { + abstract onLogin: Observable; + /** Subscribe to whether the user has valid Id/Access tokens. */ abstract authenticated$: Observable; diff --git a/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts b/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts deleted file mode 100644 index 0b0ceddc627..00000000000 --- a/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts +++ /dev/null @@ -1,134 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Injectable, inject } from '@angular/core'; -import { OAuthEvent, OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; -import { EMPTY, Observable } from 'rxjs'; -import { catchError, filter, map } from 'rxjs/operators'; -import { AppConfigValues } from '../../app-config/app-config.service'; -import { BaseAuthenticationService } from '../services/base-authentication.service'; -import { JwtHelperService } from '../services/jwt-helper.service'; -import { AuthConfigService } from '../oidc/auth-config.service'; -import { AuthService } from './auth.service'; - -@Injectable({ - providedIn: 'root' -}) -export class OIDCAuthenticationService extends BaseAuthenticationService { - private authStorage = inject(OAuthStorage); - private oauthService = inject(OAuthService); - private readonly authConfig = inject(AuthConfigService); - private readonly auth = inject(AuthService); - - readonly supportCodeFlow = true; - - constructor() { - super(); - this.alfrescoApi.alfrescoApiInitialized.subscribe(() => { - this.oauthService.events.pipe( - filter((event)=> event.type === 'token_received') - ).subscribe(()=>{ - this.onLogin.next({}); - }); - }); - } - - isEcmLoggedIn(): boolean { - return this.isLoggedIn(); - } - - isBpmLoggedIn(): boolean { - return this.isLoggedIn(); - } - - isLoggedIn(): boolean { - return this.oauthService.hasValidAccessToken() && this.oauthService.hasValidIdToken(); - } - - isLoggedInWith(_provider?: string): boolean { - return this.isLoggedIn(); - } - - isOauth(): boolean { - return this.appConfig.get(AppConfigValues.AUTHTYPE) === 'OAUTH'; - } - - isImplicitFlow(): boolean { - return !!this.appConfig.oauth2?.implicitFlow; - } - - isAuthCodeFlow(): boolean { - return !!this.appConfig.oauth2?.codeFlow; - } - - login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string; ticket: any }> { - return this.auth.baseAuthLogin(username, password).pipe( - map((response) => { - this.saveRememberMeCookie(rememberMe); - this.onLogin.next(response); - return { - type: this.appConfig.get(AppConfigValues.PROVIDERS), - ticket: response - }; - }), - catchError((err) => this.handleError(err)) - ); - } - - getEcmUsername(): string { - return (this.oauthService.getIdentityClaims() as any).preferred_username; - } - - getBpmUsername(): string { - return (this.oauthService.getIdentityClaims() as any).preferred_username; - } - - ssoImplicitLogin() { - this.oauthService.initLoginFlow(); - } - - ssoCodeFlowLogin() { - this.oauthService.initCodeFlow(); - } - - isRememberMeSet(): boolean { - return true; - } - - logout() { - this.oauthService.logOut(); - return EMPTY; - } - - getToken(): string { - return this.authStorage.getItem(JwtHelperService.USER_ACCESS_TOKEN); - } - - reset(): void { - const config = this.authConfig.loadAppConfig(); - this.auth.updateIDPConfiguration(config); - const oauth2 = this.appConfig.oauth2; - - if (config.oidc && oauth2.silentLogin) { - this.auth.login(); - } - } - - once(event: string): Observable { - return this.oauthService.events.pipe(filter(_event => _event.type === event)); - } -} diff --git a/lib/core/src/lib/auth/oidc/redirect-auth.service.ts b/lib/core/src/lib/auth/oidc/redirect-auth.service.ts index c8591d03397..96cdc5a5e18 100644 --- a/lib/core/src/lib/auth/oidc/redirect-auth.service.ts +++ b/lib/core/src/lib/auth/oidc/redirect-auth.service.ts @@ -19,13 +19,16 @@ import { Inject, Injectable } from '@angular/core'; import { AuthConfig, AUTH_CONFIG, OAuthErrorEvent, OAuthService, OAuthStorage, TokenResponse } from 'angular-oauth2-oidc'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; import { from, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, shareReplay, startWith } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators'; import { AuthService } from './auth.service'; const isPromise = (value: T | Promise): value is Promise => value && typeof (value as Promise).then === 'function'; @Injectable() export class RedirectAuthService extends AuthService { + + onLogin: Observable; + private _loadDiscoveryDocumentPromise = Promise.resolve(false); /** Subscribe to whether the user has valid Id/Access tokens. */ @@ -52,29 +55,32 @@ export class RedirectAuthService extends AuthService { ) { super(); this.authConfig = authConfig; - } - init() { this.oauthService.clearHashAfterLogin = true; this.authenticated$ = this.oauthService.events.pipe( - startWith(undefined), map(() => this.authenticated), distinctUntilChanged(), shareReplay(1) ); + this.onLogin = this.authenticated$.pipe( + filter((authenticated) => authenticated), + map(() => undefined) + ); + this.idpUnreachable$ = this.oauthService.events.pipe( filter((event): event is OAuthErrorEvent => event.type === 'discovery_document_load_error'), map((event) => event.reason as Error) ); + } + init() { if (isPromise(this.authConfig)) { return this.authConfig.then((config) => this.configureAuth(config)); } return this.configureAuth(this.authConfig); - } logout() { diff --git a/lib/core/src/lib/auth/oidc/view/authentication-confirmation/authentication-confirmation.component.ts b/lib/core/src/lib/auth/oidc/view/authentication-confirmation/authentication-confirmation.component.ts index 9492740b357..1bcf03eb951 100644 --- a/lib/core/src/lib/auth/oidc/view/authentication-confirmation/authentication-confirmation.component.ts +++ b/lib/core/src/lib/auth/oidc/view/authentication-confirmation/authentication-confirmation.component.ts @@ -24,7 +24,7 @@ import { AuthService } from '../../auth.service'; const ROUTE_DEFAULT = '/'; @Component({ - template: '', + template: '
', changeDetection: ChangeDetectionStrategy.OnPush }) export class AuthenticationConfirmationComponent { diff --git a/lib/core/src/lib/auth/public-api.ts b/lib/core/src/lib/auth/public-api.ts index b9cfb15d9ca..0cea03f7342 100644 --- a/lib/core/src/lib/auth/public-api.ts +++ b/lib/core/src/lib/auth/public-api.ts @@ -31,6 +31,10 @@ export * from './services/jwt-helper.service'; export * from './services/oauth2.service'; export * from './services/user-access.service'; +export * from './basic-auth/basic-alfresco-auth.service'; +export * from './basic-auth/process-auth'; +export * from './basic-auth/content-auth'; + export * from './interfaces/identity-user.service.interface'; export * from './interfaces/identity-group.interface'; export * from './interfaces/openid-configuration.interface'; diff --git a/lib/core/src/lib/auth/services/authentication.service.spec.ts b/lib/core/src/lib/auth/services/authentication.service.spec.ts index da88ee4263d..16f35e51919 100644 --- a/lib/core/src/lib/auth/services/authentication.service.spec.ts +++ b/lib/core/src/lib/auth/services/authentication.service.spec.ts @@ -16,21 +16,23 @@ */ import { fakeAsync, TestBed } from '@angular/core/testing'; -import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { AuthenticationService } from './authentication.service'; import { CookieService } from '../../common/services/cookie.service'; import { AppConfigService } from '../../app-config/app-config.service'; import { setupTestBed } from '../../testing/setup-test-bed'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from './oidc-authentication.service'; declare let jasmine: any; describe('AuthenticationService', () => { - let apiService: AlfrescoApiService; let authService: AuthenticationService; + let basicAlfrescoAuthService: BasicAlfrescoAuthService; let appConfigService: AppConfigService; let cookie: CookieService; + let oidcAuthenticationService: OidcAuthenticationService; setupTestBed({ imports: [ @@ -42,8 +44,9 @@ describe('AuthenticationService', () => { beforeEach(() => { sessionStorage.clear(); localStorage.clear(); - apiService = TestBed.inject(AlfrescoApiService); authService = TestBed.inject(AuthenticationService); + basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); + oidcAuthenticationService = TestBed.inject(OidcAuthenticationService); cookie = TestBed.inject(CookieService); cookie.clear(); @@ -73,6 +76,23 @@ describe('AuthenticationService', () => { }); appConfigService.load(); }); + + it('should kerberos be disabled if is oauth', () => { + spyOn(authService, 'isOauth').and.returnValue(true); + expect(authService.isKerberosEnabled()).toEqual(false); + }); + + it('should kerberos not enabled if is oauth is false and basic auth return false', () => { + spyOn(authService, 'isOauth').and.returnValue(false); + spyOn(basicAlfrescoAuthService, 'isKerberosEnabled').and.returnValue(false); + expect(authService.isKerberosEnabled()).toEqual(false); + }); + + it('should kerberos be enabled if is oauth is false and basic auth return true', () => { + spyOn(authService, 'isOauth').and.returnValue(false); + spyOn(basicAlfrescoAuthService, 'isKerberosEnabled').and.returnValue(true); + expect(authService.isKerberosEnabled()).toEqual(true); + }); }); describe('when the setting is ECM', () => { @@ -83,40 +103,30 @@ describe('AuthenticationService', () => { appConfigService.config.auth = { withCredentials: false }; appConfigService.config.providers = 'ECM'; appConfigService.load(); - apiService.reset(); }); it('should not require cookie service enabled for ECM check', () => { spyOn(cookie, 'isEnabled').and.returnValue(false); - spyOn(authService, 'isRememberMeSet').and.returnValue(false); + spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false); spyOn(authService, 'isECMProvider').and.returnValue(true); spyOn(authService, 'isOauth').and.returnValue(false); - spyOn(apiService, 'getInstance').and.callThrough(); expect(authService.isEcmLoggedIn()).toBeFalsy(); - expect(apiService.getInstance).toHaveBeenCalled(); - }); - - it('should check if loggedin on ECM in case the provider is ECM', () => { - spyOn(authService, 'isEcmLoggedIn').and.returnValue(true); - expect(authService.isLoggedInWith('ECM')).toBe(true); }); it('should require remember me set for ECM check', () => { spyOn(cookie, 'isEnabled').and.returnValue(true); - spyOn(authService, 'isRememberMeSet').and.returnValue(false); + spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false); spyOn(authService, 'isECMProvider').and.returnValue(true); spyOn(authService, 'isOauth').and.returnValue(false); - spyOn(apiService, 'getInstance').and.callThrough(); expect(authService.isEcmLoggedIn()).toBeFalsy(); - expect(apiService.getInstance).not.toHaveBeenCalled(); }); it('[ECM] should return an ECM ticket after the login done', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => { expect(authService.isLoggedIn()).toBe(true); - expect(authService.getTicketEcm()).toEqual('fake-post-ticket'); + expect(authService.getToken()).toEqual('fake-post-ticket'); expect(authService.isEcmLoggedIn()).toBe(true); disposableLogin.unsubscribe(); done(); @@ -130,7 +140,7 @@ describe('AuthenticationService', () => { }); it('[ECM] should login in the ECM if no provider are defined calling the login', fakeAsync(() => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe((loginResponse) => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe((loginResponse) => { expect(loginResponse).toEqual(fakeECMLoginResponse); disposableLogin.unsubscribe(); }); @@ -143,10 +153,10 @@ describe('AuthenticationService', () => { })); it('[ECM] should return a ticket undefined after logout', fakeAsync(() => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => { const disposableLogout = authService.logout().subscribe(() => { expect(authService.isLoggedIn()).toBe(false); - expect(authService.getTicketEcm()).toBe(null); + expect(authService.getToken()).toBe(null); expect(authService.isEcmLoggedIn()).toBe(false); disposableLogin.unsubscribe(); disposableLogout.unsubscribe(); @@ -170,21 +180,21 @@ describe('AuthenticationService', () => { }); it('[ECM] should set/get redirectUrl when provider is ECM', () => { - authService.setRedirect({ provider: 'ECM', url: 'some-url' }); + basicAlfrescoAuthService.setRedirect({ provider: 'ECM', url: 'some-url' }); - expect(authService.getRedirect()).toEqual('some-url'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url'); }); it('[ECM] should set/get redirectUrl when provider is BPM', () => { - authService.setRedirect({ provider: 'BPM', url: 'some-url' }); + basicAlfrescoAuthService.setRedirect({ provider: 'BPM', url: 'some-url' }); - expect(authService.getRedirect()).toBeNull(); + expect(basicAlfrescoAuthService.getRedirect()).toBeNull(); }); it('[ECM] should return null as redirectUrl when redirectUrl field is not set', () => { - authService.setRedirect(null); + basicAlfrescoAuthService.setRedirect(null); - expect(authService.getRedirect()).toBeNull(); + expect(basicAlfrescoAuthService.getRedirect()).toBeNull(); }); it('[ECM] should return isECMProvider true', () => { @@ -209,40 +219,30 @@ describe('AuthenticationService', () => { beforeEach(() => { appConfigService.config.providers = 'BPM'; appConfigService.load(); - apiService.reset(); }); it('should require remember me set for BPM check', () => { spyOn(cookie, 'isEnabled').and.returnValue(true); - spyOn(authService, 'isRememberMeSet').and.returnValue(false); + spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false); spyOn(authService, 'isBPMProvider').and.returnValue(true); spyOn(authService, 'isOauth').and.returnValue(false); - spyOn(apiService, 'getInstance').and.callThrough(); expect(authService.isBpmLoggedIn()).toBeFalsy(); - expect(apiService.getInstance).not.toHaveBeenCalled(); - }); - - it('should check if loggedin on BPM in case the provider is BPM', () => { - spyOn(authService, 'isBpmLoggedIn').and.returnValue(true); - expect(authService.isLoggedInWith('BPM')).toBe(true); }); it('should not require cookie service enabled for BPM check', () => { spyOn(cookie, 'isEnabled').and.returnValue(false); - spyOn(authService, 'isRememberMeSet').and.returnValue(false); + spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false); spyOn(authService, 'isBPMProvider').and.returnValue(true); - spyOn(apiService, 'getInstance').and.callThrough(); expect(authService.isBpmLoggedIn()).toBeFalsy(); - expect(apiService.getInstance).toHaveBeenCalled(); }); it('[BPM] should return an BPM ticket after the login done', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => { expect(authService.isLoggedIn()).toBe(true); // cspell: disable-next - expect(authService.getTicketBpm()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk'); + expect(authService.getToken()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk'); expect(authService.isBpmLoggedIn()).toBe(true); disposableLogin.unsubscribe(); done(); @@ -255,10 +255,10 @@ describe('AuthenticationService', () => { }); it('[BPM] should return a ticket undefined after logout', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => { const disposableLogout = authService.logout().subscribe(() => { expect(authService.isLoggedIn()).toBe(false); - expect(authService.getTicketBpm()).toBe(null); + expect(authService.getToken()).toBe(null); expect(authService.isBpmLoggedIn()).toBe(false); disposableLogout.unsubscribe(); disposableLogin.unsubscribe(); @@ -281,7 +281,7 @@ describe('AuthenticationService', () => { }, (err: any) => { expect(err).toBeDefined(); - expect(authService.getTicketBpm()).toBe(undefined); + expect(authService.getToken()).toBe(null); done(); }); @@ -291,21 +291,21 @@ describe('AuthenticationService', () => { }); it('[BPM] should set/get redirectUrl when provider is BPM', () => { - authService.setRedirect({ provider: 'BPM', url: 'some-url' }); + basicAlfrescoAuthService.setRedirect({ provider: 'BPM', url: 'some-url' }); - expect(authService.getRedirect()).toEqual('some-url'); + expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url'); }); it('[BPM] should set/get redirectUrl when provider is ECM', () => { - authService.setRedirect({ provider: 'ECM', url: 'some-url' }); + basicAlfrescoAuthService.setRedirect({ provider: 'ECM', url: 'some-url' }); - expect(authService.getRedirect()).toBeNull(); + expect(basicAlfrescoAuthService.getRedirect()).toBeNull(); }); it('[BPM] should return null as redirectUrl when redirectUrl field is not set', () => { - authService.setRedirect(null); + basicAlfrescoAuthService.setRedirect(null); - expect(authService.getRedirect()).toBeNull(); + expect(basicAlfrescoAuthService.getRedirect()).toBeNull(); }); it('[BPM] should return isECMProvider false', () => { @@ -326,11 +326,10 @@ describe('AuthenticationService', () => { beforeEach(() => { appConfigService.config.providers = 'ECM'; appConfigService.load(); - apiService.reset(); }); it('[ECM] should save the remember me cookie as a session cookie after successful login', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password', false).subscribe(() => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password', false).subscribe(() => { expect(cookie['ALFRESCO_REMEMBER_ME']).not.toBeUndefined(); expect(cookie['ALFRESCO_REMEMBER_ME'].expiration).toBeNull(); disposableLogin.unsubscribe(); @@ -345,7 +344,7 @@ describe('AuthenticationService', () => { }); it('[ECM] should save the remember me cookie as a persistent cookie after successful login', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password', true).subscribe(() => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password', true).subscribe(() => { expect(cookie['ALFRESCO_REMEMBER_ME']).not.toBeUndefined(); expect(cookie['ALFRESCO_REMEMBER_ME'].expiration).not.toBeNull(); disposableLogin.unsubscribe(); @@ -361,7 +360,7 @@ describe('AuthenticationService', () => { }); it('[ECM] should not save the remember me cookie after failed login', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe( + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe( () => {}, () => { expect(cookie['ALFRESCO_REMEMBER_ME']).toBeUndefined(); @@ -390,15 +389,14 @@ describe('AuthenticationService', () => { beforeEach(() => { appConfigService.config.providers = 'ALL'; appConfigService.load(); - apiService.reset(); }); it('[ALL] should return both ECM and BPM tickets after the login done', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => { + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => { expect(authService.isLoggedIn()).toBe(true); - expect(authService.getTicketEcm()).toEqual('fake-post-ticket'); + expect(basicAlfrescoAuthService.getTicketEcm()).toEqual('fake-post-ticket'); // cspell: disable-next - expect(authService.getTicketBpm()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk'); + expect(basicAlfrescoAuthService.getTicketBpm()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk'); expect(authService.isBpmLoggedIn()).toBe(true); expect(authService.isEcmLoggedIn()).toBe(true); disposableLogin.unsubscribe(); @@ -417,13 +415,13 @@ describe('AuthenticationService', () => { }); it('[ALL] should return login fail if only ECM call fail', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe( + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe( () => {}, () => { expect(authService.isLoggedIn()).toBe(false, 'isLoggedIn'); - expect(authService.getTicketEcm()).toBe(null, 'getTicketEcm'); + expect(authService.getToken()).toBe(null, 'getTicketEcm'); // cspell: disable-next - expect(authService.getTicketBpm()).toBe(null, 'getTicketBpm'); + expect(authService.getToken()).toBe(null, 'getTicketBpm'); expect(authService.isEcmLoggedIn()).toBe(false, 'isEcmLoggedIn'); disposableLogin.unsubscribe(); done(); @@ -439,12 +437,12 @@ describe('AuthenticationService', () => { }); it('[ALL] should return login fail if only BPM call fail', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe( + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe( () => {}, () => { expect(authService.isLoggedIn()).toBe(false); - expect(authService.getTicketEcm()).toBe(null); - expect(authService.getTicketBpm()).toBe(null); + expect(authService.getToken()).toBe(null); + expect(authService.getToken()).toBe(null); expect(authService.isBpmLoggedIn()).toBe(false); disposableLogin.unsubscribe(); done(); @@ -462,12 +460,12 @@ describe('AuthenticationService', () => { }); it('[ALL] should return ticket undefined when the credentials are wrong', (done) => { - const disposableLogin = authService.login('fake-username', 'fake-password').subscribe( + const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe( () => {}, () => { expect(authService.isLoggedIn()).toBe(false); - expect(authService.getTicketEcm()).toBe(null); - expect(authService.getTicketBpm()).toBe(null); + expect(authService.getToken()).toBe(null); + expect(authService.getToken()).toBe(null); expect(authService.isBpmLoggedIn()).toBe(false); expect(authService.isEcmLoggedIn()).toBe(false); disposableLogin.unsubscribe(); @@ -483,30 +481,6 @@ describe('AuthenticationService', () => { }); }); - it('[ALL] should set/get redirectUrl when provider is ALL', () => { - authService.setRedirect({ provider: 'ALL', url: 'some-url' }); - - expect(authService.getRedirect()).toEqual('some-url'); - }); - - it('[ALL] should set/get redirectUrl when provider is BPM', () => { - authService.setRedirect({ provider: 'BPM', url: 'some-url' }); - - expect(authService.getRedirect()).toEqual('some-url'); - }); - - it('[ALL] should set/get redirectUrl when provider is ECM', () => { - authService.setRedirect({ provider: 'ECM', url: 'some-url' }); - - expect(authService.getRedirect()).toEqual('some-url'); - }); - - it('[ALL] should return null as redirectUrl when redirectUrl field is not set', () => { - authService.setRedirect(null); - - expect(authService.getRedirect()).toBeNull(); - }); - it('[ALL] should return isECMProvider false', () => { expect(authService.isECMProvider()).toBe(false); }); @@ -519,4 +493,22 @@ describe('AuthenticationService', () => { expect(authService.isALLProvider()).toBe(true); }); }); + + describe('getUsername', () => { + it('should get the username of the authenticated user if isOAuth is true', () => { + spyOn(authService, 'isOauth').and.returnValue(true); + spyOn(oidcAuthenticationService, 'getUsername').and.returnValue('mike.portnoy'); + const username = authService.getUsername(); + expect(username).toEqual('mike.portnoy'); + }); + + it('should get the username of the authenticated user if isOAuth is false', () => { + spyOn(authService, 'isOauth').and.returnValue(false); + spyOn(oidcAuthenticationService, 'getUsername').and.returnValue('mike.portnoy'); + spyOn(basicAlfrescoAuthService, 'getUsername').and.returnValue('john.petrucci'); + const username = authService.getUsername(); + expect(username).toEqual('john.petrucci'); + }); + + }); }); diff --git a/lib/core/src/lib/auth/services/authentication.service.ts b/lib/core/src/lib/auth/services/authentication.service.ts index b7d00256d9d..0aa1e00c045 100644 --- a/lib/core/src/lib/auth/services/authentication.service.ts +++ b/lib/core/src/lib/auth/services/authentication.service.ts @@ -15,184 +15,195 @@ * limitations under the License. */ -import { Injectable, inject } from '@angular/core'; -import { Observable, from } from 'rxjs'; -import { AppConfigValues } from '../../app-config/app-config.service'; -import { map, catchError, tap } from 'rxjs/operators'; -import { JwtHelperService } from './jwt-helper.service'; -import { StorageService } from '../../common/services/storage.service'; -import { BaseAuthenticationService } from './base-authentication.service'; +import { Injectable, Injector } from '@angular/core'; +import { OidcAuthenticationService } from './oidc-authentication.service'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { Observable, Subject, from } from 'rxjs'; +import { HttpHeaders } from '@angular/common/http'; +import { AuthenticationServiceInterface } from '../interfaces/authentication-service.interface'; +import ee from 'event-emitter'; +import { RedirectAuthService } from '../oidc/redirect-auth.service'; @Injectable({ providedIn: 'root' }) -export class AuthenticationService extends BaseAuthenticationService { - private storageService = inject(StorageService); - readonly supportCodeFlow = false; +export class AuthenticationService implements AuthenticationServiceInterface, ee.Emitter { - constructor() { - super(); - this.alfrescoApi.alfrescoApiInitialized.subscribe(() => { - this.alfrescoApi.getInstance().reply('logged-in', () => { - this.onLogin.next(); - }); - }); - } + onLogin: Subject = new Subject(); + onLogout: Subject = new Subject(); - /** - * Checks if the user logged in. - * - * @returns True if logged in, false otherwise - */ - isLoggedIn(): boolean { - if (!this.isOauth() && this.cookie.isEnabled() && !this.isRememberMeSet()) { - return false; - } - return this.alfrescoApi.getInstance().isLoggedIn(); - } + constructor( + private injector: Injector, + private redirectAuthService: RedirectAuthService + ) { + this.redirectAuthService.onLogin.subscribe( + (value) => this.onLogin.next(value) + ); - isLoggedInWith(provider: string): boolean { - if (provider === 'BPM') { - return this.isBpmLoggedIn(); - } else if (provider === 'ECM') { - return this.isEcmLoggedIn(); + this.basicAlfrescoAuthService.onLogin.subscribe( + (value) => this.onLogin.next(value) + ); + + if (this.isOauth()) { + this.oidcAuthenticationService.onLogout.subscribe( + (value) => this.onLogout.next(value) + ); } else { - return this.isLoggedIn(); + this.basicAlfrescoAuthService.onLogout.subscribe( + (value) => this.onLogout.next(value) + ); } } - /** - * Does the provider support OAuth? - * - * @returns True if supported, false otherwise - */ - isOauth(): boolean { - return this.alfrescoApi.getInstance().isOauthConfiguration(); + get on(): ee.EmitterMethod { + return this.isOauth() ? this.oidcAuthenticationService.on : this.basicAlfrescoAuthService.on; } - /** - * Logs the user in. - * - * @param username Username for the login - * @param password Password for the login - * @param rememberMe Stores the user's login details if true - * @returns Object with auth type ("ECM", "BPM" or "ALL") and auth ticket - */ - login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string; ticket: any }> { - return from(this.alfrescoApi.getInstance().login(username, password)).pipe( - map((response: any) => { - this.saveRememberMeCookie(rememberMe); - this.onLogin.next(response); - return { - type: this.appConfig.get(AppConfigValues.PROVIDERS), - ticket: response - }; - }), - catchError((err) => this.handleError(err)) - ); + get off(): ee.EmitterMethod { + return this.isOauth() ? this.oidcAuthenticationService.off : this.basicAlfrescoAuthService.off; } - /** - * Logs the user in with SSO - */ - ssoImplicitLogin() { - this.alfrescoApi.getInstance().implicitLogin(); + get once(): ee.EmitterMethod { + return this.isOauth() ? this.oidcAuthenticationService.once : this.basicAlfrescoAuthService.once; } + get emit(): (type: string, ...args: any[]) => void { + return this.isOauth() ? this.oidcAuthenticationService.emit : this.basicAlfrescoAuthService.emit; + } - /** - * Logs the user out. - * - * @returns Response event called when logout is complete - */ - logout() { - return from(this.callApiLogout()).pipe( - tap((response) => { - this.onLogout.next(response); - return response; - }), - catchError((err) => this.handleError(err)) - ); + get onError(): Observable { + return this.isOauth() ? this.oidcAuthenticationService.onError : this.basicAlfrescoAuthService.onError; + } + + addTokenToHeader(requestUrl: string, headersArg?: HttpHeaders): Observable { + return this.isOauth() ? this.oidcAuthenticationService.addTokenToHeader(requestUrl, headersArg) : this.basicAlfrescoAuthService.addTokenToHeader(requestUrl, headersArg); + } + + isECMProvider(): boolean { + return this.isOauth() ? this.oidcAuthenticationService.isECMProvider() : this.basicAlfrescoAuthService.isECMProvider(); + } + + isBPMProvider(): boolean { + return this.isOauth() ? this.oidcAuthenticationService.isBPMProvider() : this.basicAlfrescoAuthService.isBPMProvider(); + } + + isALLProvider(): boolean { + return this.isOauth() ? this.oidcAuthenticationService.isALLProvider() : this.basicAlfrescoAuthService.isALLProvider(); + } + + private get oidcAuthenticationService(): OidcAuthenticationService { + return this.injector.get(OidcAuthenticationService); + } + + private get basicAlfrescoAuthService(): BasicAlfrescoAuthService { + return this.injector.get(BasicAlfrescoAuthService); } - private callApiLogout(): Promise { - if (this.alfrescoApi.getInstance()) { - return this.alfrescoApi.getInstance().logout(); + getToken(): string { + if (this.isOauth()) { + return this.oidcAuthenticationService.getToken(); + } else { + return this.basicAlfrescoAuthService.getToken(); + } + } + + isLoggedIn(): boolean { + if (this.isOauth()) { + return this.oidcAuthenticationService.isLoggedIn(); + } else { + return this.basicAlfrescoAuthService.isLoggedIn(); + } + } + + logout(): Observable { + if (this.isOauth()) { + return this.oidcAuthenticationService.logout(); + } else { + return from(this.basicAlfrescoAuthService.logout()); } - return Promise.resolve(); } - /** - * Checks if the user is logged in on an ECM provider. - * - * @returns True if logged in, false otherwise - */ isEcmLoggedIn(): boolean { - if (this.isECMProvider() || this.isALLProvider()) { - if (!this.isOauth() && this.cookie.isEnabled() && !this.isRememberMeSet()) { - return false; - } - return this.alfrescoApi.getInstance().isEcmLoggedIn(); + if (this.isOauth()) { + return this.oidcAuthenticationService.isEcmLoggedIn(); + } else { + return this.basicAlfrescoAuthService.isEcmLoggedIn(); + } + } + + isBpmLoggedIn(): boolean { + if (this.isOauth()) { + return this.oidcAuthenticationService.isBpmLoggedIn(); + } else { + return this.basicAlfrescoAuthService.isBpmLoggedIn(); + } + } + + reset(): void { + if (this.isOauth()) { + return this.oidcAuthenticationService.reset(); + } else { + return this.basicAlfrescoAuthService.reset(); + } + } + + login(username: string, password: string, rememberMe?: boolean): Observable<{ type: string; ticket: any }> { + if (this.isOauth()) { + return this.oidcAuthenticationService.loginWithPassword(username, password); + } else { + return this.basicAlfrescoAuthService.login(username, password, rememberMe); } - return false; } /** - * Checks if the user is logged in on a BPM provider. - * - * @returns True if logged in, false otherwise + * @returns the username of the authenticated user */ - isBpmLoggedIn(): boolean { - if (this.isBPMProvider() || this.isALLProvider()) { - if (!this.isOauth() && this.cookie.isEnabled() && !this.isRememberMeSet()) { - return false; - } - return this.alfrescoApi.getInstance().isBpmLoggedIn(); + getUsername(): string { + if (this.isOauth()) { + return this.oidcAuthenticationService.getUsername(); + } else { + return this.basicAlfrescoAuthService.getUsername(); } - return false; } /** - * Gets the ECM username. - * - * @returns The ECM username + * @deprecated + * @returns the logged username */ getEcmUsername(): string { - return this.alfrescoApi.getInstance().getEcmUsername(); + if (this.isOauth()) { + return this.oidcAuthenticationService.getUsername(); + } else { + return this.basicAlfrescoAuthService.getEcmUsername(); + } } /** - * Gets the BPM username - * - * @returns The BPM username + * @deprecated + * @returns the logged username */ getBpmUsername(): string { - return this.alfrescoApi.getInstance().getBpmUsername(); + if (this.isOauth()) { + return this.oidcAuthenticationService.getUsername(); + } else { + return this.basicAlfrescoAuthService.getBpmUsername(); + } } - isImplicitFlow(): boolean { - return !!this.appConfig.oauth2?.implicitFlow; + getAuthHeaders(requestUrl: string, headers: HttpHeaders): HttpHeaders { + if (this.isOauth()) { + return this.oidcAuthenticationService.getAuthHeaders(requestUrl, headers); + } else { + return this.basicAlfrescoAuthService.getAuthHeaders(requestUrl, headers); + } } - isAuthCodeFlow(): boolean { - return false; + isOauth(): boolean { + return this.basicAlfrescoAuthService.isOauth(); } - /** - * Gets the auth token. - * - * @returns Auth token string - */ - getToken(): string { - return this.storageService.getItem(JwtHelperService.USER_ACCESS_TOKEN); + isKerberosEnabled(): boolean { + return !this.isOauth() ? this.basicAlfrescoAuthService.isKerberosEnabled() : false; } - reset() { } - - once(event: string): Observable { - const alfrescoApiEvent = event === 'token_received' ? 'token_issued' : event; - return new Observable((subscriber) => { - this.alfrescoApi.getInstance().oauth2Auth.once(alfrescoApiEvent, () => subscriber.next()); - }); - } } diff --git a/lib/core/src/lib/auth/services/base-authentication.service.ts b/lib/core/src/lib/auth/services/base-authentication.service.ts index 3e6d618c02d..eaa548b20c2 100644 --- a/lib/core/src/lib/auth/services/base-authentication.service.ts +++ b/lib/core/src/lib/auth/services/base-authentication.service.ts @@ -15,76 +15,70 @@ * limitations under the License. */ -import { PeopleApi, UserProfileApi } from '@alfresco/js-api'; import { HttpHeaders } from '@angular/common/http'; import { RedirectionModel } from '../models/redirection.model'; import { Observable, Observer, ReplaySubject, throwError } from 'rxjs'; import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; -import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { CookieService } from '../../common/services/cookie.service'; import { LogService } from '../../common/services/log.service'; -import { inject } from '@angular/core'; +import { AuthenticationServiceInterface } from '../interfaces/authentication-service.interface'; +import ee from 'event-emitter'; -const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME'; -const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30; +export abstract class BaseAuthenticationService implements AuthenticationServiceInterface, ee.Emitter { -export abstract class BaseAuthenticationService { - protected alfrescoApi = inject(AlfrescoApiService); - protected appConfig = inject(AppConfigService); - protected cookie = inject(CookieService); - private logService = inject(LogService); + on: ee.EmitterMethod; + off: ee.EmitterMethod; + once: ee.EmitterMethod; + emit: (type: string, ...args: any[]) => void; - protected bearerExcludedUrls: readonly string[] = ['resources/', 'assets/', 'auth/realms', 'idp/']; protected redirectUrl: RedirectionModel = null; + onError = new ReplaySubject(1); onLogin = new ReplaySubject(1); onLogout = new ReplaySubject(1); - private _peopleApi: PeopleApi; - get peopleApi(): PeopleApi { - this._peopleApi = this._peopleApi ?? new PeopleApi(this.alfrescoApi.getInstance()); - return this._peopleApi; + constructor( + protected appConfig: AppConfigService, + protected cookie: CookieService, + private logService: LogService + ) { + ee(this); } - private _profileApi: UserProfileApi; - get profileApi(): UserProfileApi { - this._profileApi = this._profileApi ?? new UserProfileApi(this.alfrescoApi.getInstance()); - return this._profileApi; - } + abstract getAuthHeaders(requestUrl: string, header: HttpHeaders): HttpHeaders; - abstract readonly supportCodeFlow: boolean; abstract getToken(): string; + abstract isLoggedIn(): boolean; - abstract isLoggedInWith(provider: string): boolean; - abstract isOauth(): boolean; - abstract login(username: string, password: string, rememberMe?: boolean): Observable<{ type: string; ticket: any }>; - abstract ssoImplicitLogin(): void; - abstract logout(): Observable; + + abstract logout(): any; + abstract isEcmLoggedIn(): boolean; + abstract isBpmLoggedIn(): boolean; - abstract getEcmUsername(): string; - abstract getBpmUsername(): string; + abstract reset(): void; - abstract once(event: string): Observable; - getBearerExcludedUrls(): readonly string[] { - return this.bearerExcludedUrls; - } + abstract getEcmUsername(): string; + + abstract getBpmUsername(): string; /** * Adds the auth token to an HTTP header using the 'bearer' scheme. * + * @param requestUrl the request url * @param headersArg Header that will receive the token * @returns The new header with the token added */ - addTokenToHeader(headersArg?: HttpHeaders): Observable { + addTokenToHeader(requestUrl: string, headersArg?: HttpHeaders): Observable { return new Observable((observer: Observer) => { let headers = headersArg; if (!headers) { headers = new HttpHeaders(); } try { - const header = this.getAuthHeaders(headers); + + const header = this.getAuthHeaders(requestUrl, headers); observer.next(header); observer.complete(); @@ -94,50 +88,9 @@ export abstract class BaseAuthenticationService { }); } - private getAuthHeaders(header: HttpHeaders): HttpHeaders { - const authType = this.appConfig.get(AppConfigValues.AUTHTYPE, 'BASIC'); - - switch (authType) { - case 'OAUTH': - return this.addBearerToken(header); - case 'BASIC': - return this.addBasicAuth(header); - default: - return header; - } - } - - private addBearerToken(header: HttpHeaders): HttpHeaders { - const token: string = this.getToken(); - - if (!token) { - return header; - } - - return header.set('Authorization', 'bearer ' + token); - } - - private addBasicAuth(header: HttpHeaders): HttpHeaders { - const ticket: string = this.getTicketEcmBase64(); - - if (!ticket) { - return header; - } - - return header.set('Authorization', ticket); - } - - isPublicUrl(): boolean { - return this.alfrescoApi.getInstance().isPublicUrl(); - } - - /** - * Does the provider support ECM? - * - * @returns True if supported, false otherwise - */ isECMProvider(): boolean { - return this.alfrescoApi.getInstance().isEcmConfiguration(); + const provider = this.appConfig.get('providers') as string; + return provider && provider.toUpperCase() === 'ECM'; } /** @@ -146,7 +99,12 @@ export abstract class BaseAuthenticationService { * @returns True if supported, false otherwise */ isBPMProvider(): boolean { - return this.alfrescoApi.getInstance().isBpmConfiguration(); + const provider = this.appConfig.get('providers'); + if (provider && (typeof provider === 'string' || provider instanceof String)) { + return provider.toUpperCase() === 'BPM'; + } else { + return false; + } } /** @@ -155,38 +113,13 @@ export abstract class BaseAuthenticationService { * @returns True if both are supported, false otherwise */ isALLProvider(): boolean { - return this.alfrescoApi.getInstance().isEcmBpmConfiguration(); - } - - /** - * Gets the ECM ticket stored in the Storage. - * - * @returns The ticket or `null` if none was found - */ - getTicketEcm(): string | null { - return this.alfrescoApi.getInstance()?.getTicketEcm(); + const provider = this.appConfig.get('providers') as string; + return provider && provider.toUpperCase() === 'ALL'; } - /** - * Gets the BPM ticket stored in the Storage. - * - * @returns The ticket or `null` if none was found - */ - getTicketBpm(): string | null { - return this.alfrescoApi.getInstance()?.getTicketBpm(); - } - - /** - * Gets the BPM ticket from the Storage in Base 64 format. - * - * @returns The ticket or `null` if none was found - */ - getTicketEcmBase64(): string | null { - const ticket = this.alfrescoApi.getInstance()?.getTicketEcm(); - if (ticket) { - return 'Basic ' + btoa(ticket); - } - return null; + isOauthConfiguration(): boolean { + const authType = this.appConfig.get('authType') as string; + return authType === 'OAUTH'; } /** @@ -196,64 +129,12 @@ export abstract class BaseAuthenticationService { * @returns Object representing the error message */ handleError(error: any): Observable { + this.onError.next(error || 'Server error'); this.logService.error('Error when logging in', error); return throwError(error || 'Server error'); } - /** - * Does kerberos enabled? - * - * @returns True if enabled, false otherwise - */ - isKerberosEnabled(): boolean { - return this.appConfig.get(AppConfigValues.AUTH_WITH_CREDENTIALS, false); - } - - /** - * Saves the "remember me" cookie as either a long-life cookie or a session cookie. - * - * @param rememberMe Enables a long-life cookie - */ - saveRememberMeCookie(rememberMe: boolean): void { - let expiration = null; - - if (rememberMe) { - expiration = new Date(); - const time = expiration.getTime(); - const expireTime = time + REMEMBER_ME_UNTIL; - expiration.setTime(expireTime); - } - this.cookie.setItem(REMEMBER_ME_COOKIE_KEY, '1', expiration, null); - } - - /** - * Checks whether the "remember me" cookie was set or not. - * - * @returns True if set, false otherwise - */ - isRememberMeSet(): boolean { - return this.cookie.getItem(REMEMBER_ME_COOKIE_KEY) !== null; - } - - setRedirect(url?: RedirectionModel) { - this.redirectUrl = url; - } - - /** - * Gets the URL to redirect to after login. - * - * @returns The redirect URL - */ - getRedirect(): string { - const provider = this.appConfig.get(AppConfigValues.PROVIDERS); - return this.hasValidRedirection(provider) ? this.redirectUrl.url : null; - } - - private hasValidRedirection(provider: string): boolean { - return this.redirectUrl && (this.redirectUrl.provider === provider || this.hasSelectedProviderAll(provider)); - } - - private hasSelectedProviderAll(provider: string): boolean { - return this.redirectUrl && (this.redirectUrl.provider === 'ALL' || provider === 'ALL'); + isOauth(): boolean { + return this.appConfig.get(AppConfigValues.AUTHTYPE) === 'OAUTH'; } } diff --git a/lib/core/src/lib/auth/services/identity-user.service.spec.ts b/lib/core/src/lib/auth/services/identity-user.service.spec.ts index 05407fef13c..62dce422454 100644 --- a/lib/core/src/lib/auth/services/identity-user.service.spec.ts +++ b/lib/core/src/lib/auth/services/identity-user.service.spec.ts @@ -35,6 +35,7 @@ import { IdentityRoleModel } from '../models/identity-role.model'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; import { AdfHttpClient } from '../../../../api/src'; +import { StorageService } from '../../common/services/storage.service'; describe('IdentityUserService', () => { @@ -46,6 +47,7 @@ describe('IdentityUserService', () => { { id: 'id-5', name: 'MOCK-ROLE-2'} ]; + let storageService: StorageService; let service: IdentityUserService; let adfHttpClient: AdfHttpClient; let requestSpy: jasmine.Spy; @@ -57,18 +59,14 @@ describe('IdentityUserService', () => { CoreTestingModule ] }); + storageService = TestBed.inject(StorageService); service = TestBed.inject(IdentityUserService); adfHttpClient = TestBed.inject(AdfHttpClient); requestSpy = spyOn(adfHttpClient, 'request'); - - const store = {}; - - spyOn(localStorage, 'getItem').and.callFake( (key: string): string => store[key] || null); - spyOn(localStorage, 'setItem').and.callFake((key: string, value: string): string => store[key] = value); }); it('should fetch identity user info from Jwt id token', () => { - localStorage.setItem(JwtHelperService.USER_ID_TOKEN, mockToken); + storageService.setItem(JwtHelperService.USER_ID_TOKEN, mockToken); const user = service.getCurrentUserInfo(); expect(user).toBeDefined(); expect(user.firstName).toEqual('John'); @@ -78,7 +76,7 @@ describe('IdentityUserService', () => { }); it('should fallback on Jwt access token for identity user info', () => { - localStorage.setItem(JwtHelperService.USER_ACCESS_TOKEN, mockToken); + storageService.setItem(JwtHelperService.USER_ACCESS_TOKEN, mockToken); const user = service.getCurrentUserInfo(); expect(user).toBeDefined(); expect(user.firstName).toEqual('John'); diff --git a/lib/core/src/lib/auth/services/oidc-authentication.service.ts b/lib/core/src/lib/auth/services/oidc-authentication.service.ts new file mode 100644 index 00000000000..508e1c3abe1 --- /dev/null +++ b/lib/core/src/lib/auth/services/oidc-authentication.service.ts @@ -0,0 +1,194 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; +import { EMPTY, Observable, defer } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; +import { OauthConfigModel } from '../models/oauth-config.model'; +import { BaseAuthenticationService } from './base-authentication.service'; +import { CookieService } from '../../common/services/cookie.service'; +import { JwtHelperService } from './jwt-helper.service'; +import { LogService } from '../../common/services/log.service'; +import { AuthConfigService } from '../oidc/auth-config.service'; +import { AuthService } from '../oidc/auth.service'; +import { Minimatch } from 'minimatch'; +import { HttpHeaders } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class OidcAuthenticationService extends BaseAuthenticationService { + + constructor( + appConfig: AppConfigService, + cookie: CookieService, + logService: LogService, + private jwtHelperService: JwtHelperService, + private authStorage: OAuthStorage, + private oauthService: OAuthService, + private readonly authConfig: AuthConfigService, + private readonly auth: AuthService + ) { + super(appConfig, cookie, logService); + } + + isEcmLoggedIn(): boolean { + if (this.isECMProvider() || this.isALLProvider()) { + return this.isLoggedIn(); + } + return false; + + } + + isBpmLoggedIn(): boolean { + if (this.isBPMProvider() || this.isALLProvider()) { + return this.isLoggedIn(); + } + return false; + } + + isLoggedIn(): boolean { + return this.oauthService.hasValidAccessToken() && this.oauthService.hasValidIdToken(); + } + + hasValidAccessToken(): boolean { + return this.oauthService.hasValidAccessToken(); + } + + hasValidIdToken(): boolean { + return this.oauthService.hasValidIdToken(); + } + + isImplicitFlow() { + const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get(AppConfigValues.OAUTHCONFIG, null)); + return !!oauth2?.implicitFlow; + } + + isAuthCodeFlow() { + const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get(AppConfigValues.OAUTHCONFIG, null)); + return !!oauth2?.codeFlow; + } + + login(username: string, password: string): Observable<{ type: string; ticket: any }> { + return this.auth.baseAuthLogin(username, password).pipe( + map((response) => { + this.onLogin.next(response); + return { + type: this.appConfig.get(AppConfigValues.PROVIDERS), + ticket: response + }; + }), + catchError((err) => this.handleError(err)) + ); + } + + loginWithPassword(username: string, password: string): Observable<{ type: string; ticket: any }> { + return defer(async () => { + try { + await this.authConfig.loadConfig(); + await this.oauthService.loadDiscoveryDocument(); + await this.oauthService.fetchTokenUsingPasswordFlowAndLoadUserProfile(username, password); + await this.oauthService.refreshToken(); + const accessToken = this.oauthService.getAccessToken(); + this.onLogin.next(accessToken); + + return { + type: this.appConfig.get(AppConfigValues.PROVIDERS) as string, + ticket: accessToken + }; + } catch (err) { + throw this.handleError(err); + } + }); + } + + getUsername(){ + return this.jwtHelperService.getValueFromLocalToken(JwtHelperService.USER_PREFERRED_USERNAME); + } + + /** + * @deprecated + * @returns the logged username + */ + getEcmUsername(): string { + return this.getUsername(); + } + + /** + * @deprecated + * @returns the logged username + */ + getBpmUsername(): string { + return this.getUsername(); + } + + ssoImplicitLogin() { + this.auth.login(); + } + + ssoCodeFlowLogin() { + this.oauthService.initCodeFlow(); + } + + isRememberMeSet(): boolean { + return true; + } + + logout() { + this.oauthService.logOut(); + return EMPTY; + } + + getToken(): string { + return this.authStorage.getItem(JwtHelperService.USER_ACCESS_TOKEN); + } + + reset(): void { + const config = this.authConfig.loadAppConfig(); + this.auth.updateIDPConfiguration(config); + } + + isPublicUrl(): boolean { + const oauth2 = this.appConfig.get(AppConfigValues.OAUTHCONFIG, null); + + if (Array.isArray(oauth2.publicUrls)) { + return oauth2.publicUrls.length > 0 && + oauth2.publicUrls.some((urlPattern: string) => { + const minimatch = new Minimatch(urlPattern); + return minimatch.match(window.location.href); + }); + } + return false; + } + + getAuthHeaders(_requestUrl: string, header: HttpHeaders): HttpHeaders { + return this.addBearerToken(header); + } + + private addBearerToken(header: HttpHeaders): HttpHeaders { + const token: string = this.getToken(); + + if (!token) { + return header; + } + + return header.set('Authorization', 'bearer ' + token); + } + +} diff --git a/lib/core/src/lib/card-view/components/card-view-keyvaluepairsitem/card-view-keyvaluepairsitem.component.spec.ts b/lib/core/src/lib/card-view/components/card-view-keyvaluepairsitem/card-view-keyvaluepairsitem.component.spec.ts index 433bff1d6f3..639a236ecff 100644 --- a/lib/core/src/lib/card-view/components/card-view-keyvaluepairsitem/card-view-keyvaluepairsitem.component.spec.ts +++ b/lib/core/src/lib/card-view/components/card-view-keyvaluepairsitem/card-view-keyvaluepairsitem.component.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CardViewKeyValuePairsItemModel } from '../../models/card-view-keyvaluepairs.model'; import { CardViewKeyValuePairsItemComponent } from './card-view-keyvaluepairsitem.component'; @@ -122,57 +122,5 @@ describe('CardViewKeyValuePairsItemComponent', () => { expect(cardViewUpdateService.update).toHaveBeenCalled(); expect(component.property.value.length).toBe(0); }); - - it('should update property on input blur', waitForAsync(() => { - spyOn(cardViewUpdateService, 'update'); - component.ngOnChanges(); - fixture.detectChanges(); - - const addButton = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__add-btn')); - addButton.triggerEventHandler('click', null); - fixture.detectChanges(); - - const nameInput = fixture.debugElement.query(By.css(`[data-automation-id="card-${component.property.key}-name-input-0"]`)); - const valueInput = fixture.debugElement.query(By.css(`[data-automation-id="card-${component.property.key}-value-input-0"]`)); - - nameInput.nativeElement.value = mockData[0].name; - nameInput.nativeElement.dispatchEvent(new Event('input')); - valueInput.nativeElement.value = mockData[0].value; - valueInput.nativeElement.dispatchEvent(new Event('input')); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - - valueInput.triggerEventHandler('blur', null); - fixture.detectChanges(); - - expect(cardViewUpdateService.update).toHaveBeenCalled(); - expect(JSON.stringify(component.property.value)).toBe(JSON.stringify(mockData)); - }); - })); - - it('should not update property if at least one input is empty on blur', waitForAsync(() => { - spyOn(cardViewUpdateService, 'update'); - component.ngOnChanges(); - fixture.detectChanges(); - - const addButton = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__add-btn')); - addButton.triggerEventHandler('click', null); - fixture.detectChanges(); - - const valueInput = fixture.debugElement.query(By.css(`[data-automation-id="card-${component.property.key}-value-input-0"]`)); - - valueInput.nativeElement.value = mockData[0].value; - valueInput.nativeElement.dispatchEvent(new Event('input')); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - - valueInput.triggerEventHandler('blur', null); - fixture.detectChanges(); - - expect(cardViewUpdateService.update).not.toHaveBeenCalled(); - }); - })); }); }); diff --git a/lib/core/src/lib/common/mock/app-config.service.mock.ts b/lib/core/src/lib/common/mock/app-config.service.mock.ts index e9a63d2bac6..6dbb6c22de6 100644 --- a/lib/core/src/lib/common/mock/app-config.service.mock.ts +++ b/lib/core/src/lib/common/mock/app-config.service.mock.ts @@ -19,12 +19,14 @@ import { Injectable } from '@angular/core'; import { AppConfigService, Status } from '../../app-config/app-config.service'; import { HttpClient } from '@angular/common/http'; import { ExtensionService } from '@alfresco/adf-extensions'; + @Injectable() export class AppConfigServiceMock extends AppConfigService { config: any = { application: { - name: 'Alfresco ADF Application' + name: 'Alfresco ADF Application', + storagePrefix: 'ADF_APP' }, ecmHost: 'http://{hostname}{:port}/ecm', bpmHost: 'http://{hostname}{:port}/bpm', @@ -35,11 +37,12 @@ export class AppConfigServiceMock extends AppConfigService { super(http, extensionService); } - load(): Promise { + load(callback?: (...args: any[]) => any): Promise { return new Promise((resolve) => { this.status = Status.LOADED; - this.onDataLoaded(this.config); + callback?.(); resolve(this.config); + this.onDataLoaded(); }); } } diff --git a/lib/core/src/lib/common/services/storage.service.spec.ts b/lib/core/src/lib/common/services/storage.service.spec.ts index a20f29e6e01..755c8f1d567 100644 --- a/lib/core/src/lib/common/services/storage.service.spec.ts +++ b/lib/core/src/lib/common/services/storage.service.spec.ts @@ -21,7 +21,6 @@ import { StorageService } from '../../common/services/storage.service'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { AppConfigServiceMock } from '../mock/app-config.service.mock'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreModule } from '../../core.module'; describe('StorageService', () => { @@ -34,8 +33,6 @@ describe('StorageService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - TranslateModule.forRoot(), - CoreModule.forRoot(), CoreTestingModule ] }); @@ -87,6 +84,7 @@ describe('StorageService', () => { ] }); appConfig = TestBed.inject(AppConfigService); + appConfig.config = { application: { storagePrefix: '' diff --git a/lib/core/src/lib/common/services/storage.service.ts b/lib/core/src/lib/common/services/storage.service.ts index 71c45dd12ee..e4daedd2307 100644 --- a/lib/core/src/lib/common/services/storage.service.ts +++ b/lib/core/src/lib/common/services/storage.service.ts @@ -81,7 +81,7 @@ export class StorageService { */ removeItem(key: string) { if (this.useLocalStorage) { - localStorage.removeItem(this.prefix + key); + localStorage.removeItem(`${this.prefix}` + key); } else { delete this.memoryStore[this.prefix + key]; } diff --git a/lib/core/src/lib/common/utils/object-utils.ts b/lib/core/src/lib/common/utils/object-utils.ts index f8808473a9c..cd63ca3b697 100644 --- a/lib/core/src/lib/common/utils/object-utils.ts +++ b/lib/core/src/lib/common/utils/object-utils.ts @@ -26,7 +26,7 @@ export class ObjectUtils { */ static getValue(target: any, key: string): any { - if (!target) { + if (!target || !key) { return undefined; } diff --git a/lib/core/src/lib/core.module.ts b/lib/core/src/lib/core.module.ts index 61d77b0acbb..60fc0a87ce9 100644 --- a/lib/core/src/lib/core.module.ts +++ b/lib/core/src/lib/core.module.ts @@ -53,9 +53,8 @@ import { ExtensionsModule } from '@alfresco/adf-extensions'; import { directionalityConfigFactory } from './common/services/directionality-config-factory'; import { DirectionalityConfigService } from './common/services/directionality-config.service'; import { SearchTextModule } from './search-text/search-text-input.module'; -import { AdfHttpClient, AlfrescoJsClientsModule } from '@alfresco/adf-core/api'; +import { AdfHttpClient } from '@alfresco/adf-core/api'; import { AuthenticationInterceptor, Authentication } from '@alfresco/adf-core/auth'; -import { LegacyApiClientModule } from './api-factories/legacy-api-client.module'; import { HttpClientModule, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthenticationService } from './auth/services/authentication.service'; import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar'; @@ -101,8 +100,6 @@ import { AdfDateTimeFnsAdapter } from './common/utils/datetime-fns-adapter'; NotificationHistoryModule, SearchTextModule, BlankPageModule, - LegacyApiClientModule, - AlfrescoJsClientsModule, HttpClientModule, HttpClientXsrfModule.withOptions({ cookieName: 'CSRF-TOKEN', diff --git a/lib/core/src/lib/directives/logout.directive.ts b/lib/core/src/lib/directives/logout.directive.ts index b1e54587f47..e4d446abaf1 100644 --- a/lib/core/src/lib/directives/logout.directive.ts +++ b/lib/core/src/lib/directives/logout.directive.ts @@ -37,7 +37,7 @@ export class LogoutDirective implements OnInit { private renderer: Renderer2, private router: Router, private appConfig: AppConfigService, - private auth: AuthenticationService) { + private authenticationService: AuthenticationService) { } ngOnInit() { @@ -58,14 +58,14 @@ export class LogoutDirective implements OnInit { } logout() { - this.auth.logout().subscribe( + this.authenticationService.logout().subscribe( () => this.redirectToUri(), () => this.redirectToUri() ); } redirectToUri() { - if (this.enableRedirect && !this.auth.isOauth()) { + if (this.enableRedirect && !this.authenticationService.isOauth()) { const redirectRoute = this.getRedirectUri(); this.router.navigate([redirectRoute]); } diff --git a/lib/core/src/lib/login/components/login-dialog-panel.component.spec.ts b/lib/core/src/lib/login/components/login-dialog-panel.component.spec.ts index 8a593594096..8d6d12a338c 100644 --- a/lib/core/src/lib/login/components/login-dialog-panel.component.spec.ts +++ b/lib/core/src/lib/login/components/login-dialog-panel.component.spec.ts @@ -16,11 +16,12 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AuthenticationService } from '../../auth/services/authentication.service'; import { LoginDialogPanelComponent } from './login-dialog-panel.component'; import { of } from 'rxjs'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; +import { BasicAlfrescoAuthService } from '../../auth/basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service'; describe('LoginDialogPanelComponent', () => { let component: LoginDialogPanelComponent; @@ -28,19 +29,23 @@ describe('LoginDialogPanelComponent', () => { let element: HTMLElement; let usernameInput: HTMLInputElement; let passwordInput: HTMLInputElement; - let authService: AuthenticationService; + let basicAlfrescoAuthService: BasicAlfrescoAuthService; beforeEach(async () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), CoreTestingModule + ], + providers: [ + { provide: OidcAuthenticationService, useValue: {}} ] }); fixture = TestBed.createComponent(LoginDialogPanelComponent); + basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); + element = fixture.nativeElement; component = fixture.componentInstance; - authService = TestBed.inject(AuthenticationService); fixture.detectChanges(); await fixture.whenStable(); @@ -76,7 +81,7 @@ describe('LoginDialogPanelComponent', () => { expect(event.token.ticket).toBe('ticket'); done(); }); - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); loginWithCredentials('fake-username', 'fake-password'); }); diff --git a/lib/core/src/lib/login/components/login.component.spec.ts b/lib/core/src/lib/login/components/login.component.spec.ts index 58bdc107591..40cdcd4bd14 100644 --- a/lib/core/src/lib/login/components/login.component.spec.ts +++ b/lib/core/src/lib/login/components/login.component.spec.ts @@ -25,10 +25,11 @@ import { AuthenticationService } from '../../auth/services/authentication.servic import { LoginErrorEvent } from '../models/login-error.event'; import { LoginSuccessEvent } from '../models/login-success.event'; import { LoginComponent } from './login.component'; -import { of, throwError } from 'rxjs'; -import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { EMPTY, of, throwError } from 'rxjs'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { LogService } from '../../common/services/log.service'; +import { BasicAlfrescoAuthService } from '../../auth/basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service'; describe('LoginComponent', () => { let component: LoginComponent; @@ -38,7 +39,7 @@ describe('LoginComponent', () => { let router: Router; let userPreferences: UserPreferencesService; let appConfigService: AppConfigService; - let alfrescoApiService: AlfrescoApiService; + let basicAlfrescoAuthService: BasicAlfrescoAuthService; let usernameInput; let passwordInput; @@ -60,6 +61,16 @@ describe('LoginComponent', () => { TestBed.configureTestingModule({ imports: [ CoreTestingModule + ], + providers: [ + { + provide: OidcAuthenticationService, useValue: { + ssoImplicitLogin: () => { }, + isPublicUrl: () => false, + hasValidIdToken: () => false, + isLoggedIn: () => false + } + } ] }); fixture = TestBed.createComponent(LoginComponent); @@ -69,11 +80,11 @@ describe('LoginComponent', () => { component.showRememberMe = true; component.showLoginActions = true; + basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); authService = TestBed.inject(AuthenticationService); router = TestBed.inject(Router); userPreferences = TestBed.inject(UserPreferencesService); appConfigService = TestBed.inject(AppConfigService); - alfrescoApiService = TestBed.inject(AlfrescoApiService); const logService = TestBed.inject(LogService); spyOn(logService, 'error'); @@ -111,7 +122,7 @@ describe('LoginComponent', () => { }); it('should redirect to route on successful login', () => { - spyOn(authService, 'login').and.returnValue( + spyOn(basicAlfrescoAuthService, 'login').and.returnValue( of({ type: 'type', ticket: 'ticket' }) ); const redirect = '/home'; @@ -161,10 +172,10 @@ describe('LoginComponent', () => { appConfigService.config = {}; appConfigService.config.providers = 'ECM'; - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); const redirect = '/home'; component.successRoute = redirect; - authService.setRedirect({ provider: 'ECM', url: 'some-route' }); + basicAlfrescoAuthService.setRedirect({ provider: 'ECM', url: 'some-route' }); spyOn(router, 'navigateByUrl'); @@ -174,8 +185,7 @@ describe('LoginComponent', () => { it('should update user preferences upon login', async () => { spyOn(userPreferences, 'setStoragePrefix').and.callThrough(); - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); - spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.resolve()); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); component.success.subscribe(() => { expect(userPreferences.setStoragePrefix).toHaveBeenCalledWith('fake-username'); @@ -206,14 +216,14 @@ describe('LoginComponent', () => { }); it('should be changed back to the default after a failed login attempt', () => { - spyOn(authService, 'login').and.returnValue(throwError('Fake server error')); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError('Fake server error')); loginWithCredentials('fake-wrong-username', 'fake-wrong-password'); expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.LOGIN'); }); it('should be changed to the "welcome key" after a successful login attempt', () => { - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); loginWithCredentials('fake-username', 'fake-password'); expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.WELCOME'); @@ -295,12 +305,12 @@ describe('LoginComponent', () => { }); it('should be taken into consideration during login attempt', fakeAsync(() => { - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); component.rememberMe = false; loginWithCredentials('fake-username', 'fake-password'); - expect(authService.login).toHaveBeenCalledWith('fake-username', 'fake-password', false); + expect(basicAlfrescoAuthService.login).toHaveBeenCalledWith('fake-username', 'fake-password', false); })); }); @@ -469,7 +479,7 @@ describe('LoginComponent', () => { }); it('should return error with a wrong username', (done) => { - spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.reject(new Error('login error'))); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError(new Error())); component.error.subscribe(() => { fixture.detectChanges(); @@ -484,7 +494,7 @@ describe('LoginComponent', () => { }); it('should return error with a wrong password', (done) => { - spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.reject(new Error('login error'))); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError(new Error())); component.error.subscribe(() => { fixture.detectChanges(); @@ -500,7 +510,7 @@ describe('LoginComponent', () => { }); it('should return error with a wrong username and password', (done) => { - spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.reject(new Error('login error'))); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError(new Error())); component.error.subscribe(() => { fixture.detectChanges(); @@ -516,7 +526,7 @@ describe('LoginComponent', () => { }); it('should return CORS error when server CORS error occurs', (done) => { - spyOn(authService, 'login').and.returnValue(throwError({ + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError({ error: { crossDomain: true, message: 'ERROR: the network is offline, Origin is not allowed by Access-Control-Allow-Origin' @@ -537,7 +547,7 @@ describe('LoginComponent', () => { }); it('should return CSRF error when server CSRF error occurs', fakeAsync(() => { - spyOn(authService, 'login') + spyOn(basicAlfrescoAuthService, 'login') .and.returnValue(throwError({ message: 'ERROR: Invalid CSRF-token', status: 403 })); component.error.subscribe(() => { @@ -552,7 +562,7 @@ describe('LoginComponent', () => { })); it('should return ECM read-only error when error occurs', fakeAsync(() => { - spyOn(authService, 'login') + spyOn(basicAlfrescoAuthService, 'login') .and.returnValue( throwError( { @@ -600,7 +610,7 @@ describe('LoginComponent', () => { }); it('should return success event after the login have succeeded', (done) => { - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); expect(component.isError).toBe(false); @@ -616,7 +626,7 @@ describe('LoginComponent', () => { }); it('should emit success event after the login has succeeded and discard password', fakeAsync(() => { - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' })); component.success.subscribe((event) => { fixture.detectChanges(); @@ -631,7 +641,7 @@ describe('LoginComponent', () => { })); it('should emit error event after the login has failed', fakeAsync(() => { - spyOn(authService, 'login').and.returnValue(throwError('Fake server error')); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError('Fake server error')); component.error.subscribe((error) => { fixture.detectChanges(); @@ -668,7 +678,7 @@ describe('LoginComponent', () => { }); it('should emit only the username and not the password as part of the executeSubmit', fakeAsync(() => { - spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.resolve()); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(EMPTY); component.executeSubmit.subscribe((res) => { fixture.detectChanges(); @@ -688,7 +698,6 @@ describe('LoginComponent', () => { beforeEach(() => { appConfigService.config.oauth2 = { implicitFlow: true, silentLogin: false }; appConfigService.load(); - alfrescoApiService.reset(); }); it('should not show login username and password if SSO implicit flow is active', fakeAsync(() => { diff --git a/lib/core/src/lib/login/components/login.component.ts b/lib/core/src/lib/login/components/login.component.ts index 5b3762e1c6b..222d84bc869 100644 --- a/lib/core/src/lib/login/components/login.component.ts +++ b/lib/core/src/lib/login/components/login.component.ts @@ -29,6 +29,8 @@ import { AppConfigService, AppConfigValues } from '../../app-config/app-config.s import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { BasicAlfrescoAuthService } from '../../auth/basic-auth/basic-alfresco-auth.service'; +import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service'; // eslint-disable-next-line no-shadow enum LoginSteps { @@ -52,7 +54,7 @@ interface LoginFormValues { templateUrl: './login.component.html', styleUrls: ['./login.component.scss'], encapsulation: ViewEncapsulation.None, - host: { class: 'adf-login' } + host: {class: 'adf-login'} }) export class LoginComponent implements OnInit, OnDestroy { isPasswordShow: boolean = false; @@ -129,6 +131,8 @@ export class LoginComponent implements OnInit, OnDestroy { constructor( private _fb: UntypedFormBuilder, private authService: AuthenticationService, + private basicAlfrescoAuthService: BasicAlfrescoAuthService, + private oidcAuthenticationService: OidcAuthenticationService, private translateService: TranslationService, private router: Router, private appConfig: AppConfigService, @@ -160,7 +164,7 @@ export class LoginComponent implements OnInit, OnDestroy { const url = params['redirectUrl']; const provider = this.appConfig.get(AppConfigValues.PROVIDERS); - this.authService.setRedirect({ provider, url }); + this.basicAlfrescoAuthService.setRedirect({provider, url}); }); } @@ -181,7 +185,7 @@ export class LoginComponent implements OnInit, OnDestroy { } redirectToImplicitLogin() { - this.authService.ssoImplicitLogin(); + this.oidcAuthenticationService.ssoImplicitLogin(); } /** @@ -193,12 +197,13 @@ export class LoginComponent implements OnInit, OnDestroy { this.disableError(); const args = new LoginSubmitEvent({ - controls: { username: this.form.controls.username } + controls: {username: this.form.controls.username} }); this.executeSubmit.emit(args); if (!args.defaultPrevented) { this.actualLoginStep = LoginSteps.Checking; + this.performLogin(values); } } @@ -207,7 +212,7 @@ export class LoginComponent implements OnInit, OnDestroy { if (this.authService.isLoggedIn()) { this.router.navigate([this.successRoute]); } - this.authService.ssoImplicitLogin(); + this.oidcAuthenticationService.ssoImplicitLogin(); } /** @@ -237,30 +242,31 @@ export class LoginComponent implements OnInit, OnDestroy { } } - performLogin(values: LoginFormValues) { - this.authService.login(values.username, values.password, this.rememberMe).subscribe( - (token) => { - const redirectUrl = this.authService.getRedirect(); - - this.actualLoginStep = LoginSteps.Welcome; - this.userPreferences.setStoragePrefix(values.username); - values.password = null; - this.success.emit(new LoginSuccessEvent(token, values.username, null)); - - if (redirectUrl) { - this.authService.setRedirect(null); - this.router.navigateByUrl(redirectUrl); - } else if (this.successRoute) { - this.router.navigate([this.successRoute]); + performLogin(values: { username: string; password: string }) { + this.authService.login(values.username, values.password, this.rememberMe) + .subscribe( + async (token: any) => { + const redirectUrl = this.basicAlfrescoAuthService.getRedirect(); + + this.actualLoginStep = LoginSteps.Welcome; + this.userPreferences.setStoragePrefix(values.username); + values.password = null; + this.success.emit(new LoginSuccessEvent(token, values.username, null)); + + if (redirectUrl) { + this.basicAlfrescoAuthService.setRedirect(null); + await this.router.navigateByUrl(redirectUrl); + } else if (this.successRoute) { + await this.router.navigate([this.successRoute]); + } + }, + (err: any) => { + this.actualLoginStep = LoginSteps.Landing; + this.displayErrorMessage(err); + this.isError = true; + this.error.emit(new LoginErrorEvent(err)); } - }, - (err: any) => { - this.actualLoginStep = LoginSteps.Landing; - this.displayErrorMessage(err); - this.isError = true; - this.error.emit(new LoginErrorEvent(err)); - } - ); + ); } /** diff --git a/lib/core/src/lib/login/directives/login-footer.directive.spec.ts b/lib/core/src/lib/login/directives/login-footer.directive.spec.ts index bce4351a80c..bde2728e773 100644 --- a/lib/core/src/lib/login/directives/login-footer.directive.spec.ts +++ b/lib/core/src/lib/login/directives/login-footer.directive.spec.ts @@ -20,6 +20,7 @@ import { LoginComponent } from '../components/login.component'; import { LoginFooterDirective } from './login-footer.directive'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; +import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service'; describe('LoginFooterDirective', () => { let fixture: ComponentFixture; @@ -31,6 +32,11 @@ describe('LoginFooterDirective', () => { imports: [ TranslateModule.forRoot(), CoreTestingModule + ], + providers: [ + { + provide: OidcAuthenticationService, useValue: {} + } ] }); fixture = TestBed.createComponent(LoginComponent); diff --git a/lib/core/src/lib/login/directives/login-header.directive.spec.ts b/lib/core/src/lib/login/directives/login-header.directive.spec.ts index 635eca7c570..c255e4c9ed7 100644 --- a/lib/core/src/lib/login/directives/login-header.directive.spec.ts +++ b/lib/core/src/lib/login/directives/login-header.directive.spec.ts @@ -20,6 +20,7 @@ import { LoginComponent } from '../components/login.component'; import { LoginHeaderDirective } from './login-header.directive'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; +import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service'; describe('LoginHeaderDirective', () => { let fixture: ComponentFixture; @@ -31,6 +32,9 @@ describe('LoginHeaderDirective', () => { imports: [ TranslateModule.forRoot(), CoreTestingModule + ], + providers: [ + { provide: OidcAuthenticationService, useValue: {} } ] }); fixture = TestBed.createComponent(LoginComponent); diff --git a/lib/core/src/lib/snackbar-content/snackbar-content.component.spec.ts b/lib/core/src/lib/snackbar-content/snackbar-content.component.spec.ts index cc8a2703c7f..d9bff92a839 100644 --- a/lib/core/src/lib/snackbar-content/snackbar-content.component.spec.ts +++ b/lib/core/src/lib/snackbar-content/snackbar-content.component.spec.ts @@ -16,12 +16,12 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SnackbarContentComponent } from '@alfresco/adf-core'; import { MatIcon, MatIconModule } from '@angular/material/icon'; import { MAT_SNACK_BAR_DATA, MatSnackBarModule, MatSnackBarRef } from '@angular/material/snack-bar'; import { MatButtonModule } from '@angular/material/button'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; +import { SnackbarContentComponent } from './snackbar-content.component'; describe('SnackbarContentComponent', () => { let component: SnackbarContentComponent; diff --git a/lib/core/src/lib/testing/automation.service.ts b/lib/core/src/lib/testing/automation.service.ts index dc8cf0dd0f7..33acfba051d 100644 --- a/lib/core/src/lib/testing/automation.service.ts +++ b/lib/core/src/lib/testing/automation.service.ts @@ -36,7 +36,8 @@ export class CoreAutomationService { private userPreferencesService: UserPreferencesService, private storageService: StorageService, private auth: AuthenticationService - ) {} + ) { + } setup() { const adfProxy = window['adf'] || {}; diff --git a/lib/core/src/lib/testing/core.story.module.ts b/lib/core/src/lib/testing/core.story.module.ts index e368b8f4097..516804b03ff 100644 --- a/lib/core/src/lib/testing/core.story.module.ts +++ b/lib/core/src/lib/testing/core.story.module.ts @@ -20,9 +20,11 @@ import { CoreModule } from '../core.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { provideTranslations } from '../translation/translation.service'; +import { AuthModule } from '../../../src/lib/auth/oidc/auth.module'; @NgModule({ imports: [ + AuthModule.forRoot(), TranslateModule.forRoot(), CoreModule.forRoot(), BrowserAnimationsModule diff --git a/lib/core/src/lib/testing/core.testing.module.ts b/lib/core/src/lib/testing/core.testing.module.ts index a90762bedbf..6226ae618aa 100644 --- a/lib/core/src/lib/testing/core.testing.module.ts +++ b/lib/core/src/lib/testing/core.testing.module.ts @@ -32,9 +32,11 @@ import { CookieServiceMock } from '../mock/cookie.service.mock'; import { HttpClientModule } from '@angular/common/http'; import { directionalityConfigFactory } from '../common/services/directionality-config-factory'; import { DirectionalityConfigService } from '../common/services/directionality-config.service'; +import { AuthModule } from '../auth'; @NgModule({ imports: [ + AuthModule.forRoot({ useHash: true }), NoopAnimationsModule, RouterTestingModule, HttpClientModule, diff --git a/lib/core/src/lib/translation/translate-loader.spec.ts b/lib/core/src/lib/translation/translate-loader.spec.ts index a02da252533..8325d456922 100644 --- a/lib/core/src/lib/translation/translate-loader.spec.ts +++ b/lib/core/src/lib/translation/translate-loader.spec.ts @@ -20,6 +20,7 @@ import { TranslateLoaderService } from './translate-loader.service'; import { TranslationService } from './translation.service'; import { TranslateModule } from '@ngx-translate/core'; import { CoreModule } from '../core.module'; +import { AuthModule } from '../auth'; declare let jasmine: any; @@ -30,6 +31,7 @@ describe('TranslateLoader', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ + AuthModule.forRoot({ useHash: true }), TranslateModule.forRoot(), CoreModule.forRoot() ], diff --git a/lib/core/src/lib/viewer/components/download-prompt-dialog/download-prompt-dialog.component.spec.ts b/lib/core/src/lib/viewer/components/download-prompt-dialog/download-prompt-dialog.component.spec.ts index 3500c481a32..ab1e645ccc1 100644 --- a/lib/core/src/lib/viewer/components/download-prompt-dialog/download-prompt-dialog.component.spec.ts +++ b/lib/core/src/lib/viewer/components/download-prompt-dialog/download-prompt-dialog.component.spec.ts @@ -17,10 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CoreTestingModule, DownloadPromptDialogComponent, DownloadPromptActions } from '@alfresco/adf-core'; import { By } from '@angular/platform-browser'; import { MatDialogRef } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; +import { DownloadPromptDialogComponent } from './download-prompt-dialog.component'; +import { CoreTestingModule } from '../../../testing/core.testing.module'; +import { DownloadPromptActions } from '../../models/download-prompt.actions'; const mockDialog = { close: jasmine.createSpy('close') diff --git a/lib/insights/src/lib/testing/insights.testing.module.ts b/lib/insights/src/lib/testing/insights.testing.module.ts index 573e5d6ed29..37cd940f62d 100644 --- a/lib/insights/src/lib/testing/insights.testing.module.ts +++ b/lib/insights/src/lib/testing/insights.testing.module.ts @@ -26,11 +26,12 @@ import { AppConfigServiceMock, TranslationService, TranslationMock, - CoreModule + CoreModule, AuthModule } from '@alfresco/adf-core'; @NgModule({ imports: [ + AuthModule.forRoot({ useHash: true }), NoopAnimationsModule, TranslateModule, CoreModule.forRoot(), diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts index 50a2487ad08..4a970df4e36 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts @@ -25,7 +25,7 @@ import { FormModel, FormOutcomeEvent, FormOutcomeModel, FormRenderingService, FormService, - UploadWidgetContentLinkModel, WidgetVisibilityService, provideTranslations + UploadWidgetContentLinkModel, WidgetVisibilityService, provideTranslations, AuthModule } from '@alfresco/adf-core'; import { Node } from '@alfresco/js-api'; import { ESCAPE } from '@angular/cdk/keycodes'; @@ -1151,6 +1151,7 @@ describe('Multilingual Form', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ + AuthModule.forRoot({ useHash: true }), NoopAnimationsModule, TranslateModule.forRoot(), CoreModule.forRoot(), @@ -1226,6 +1227,7 @@ describe('retrieve metadata on submit', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ + AuthModule.forRoot({ useHash: true }), NoopAnimationsModule, TranslateModule.forRoot(), CoreModule.forRoot(), diff --git a/lib/process-services-cloud/src/lib/people/services/identity-user.service.spec.ts b/lib/process-services-cloud/src/lib/people/services/identity-user.service.spec.ts index 30e449eef3d..fa9cdecad2b 100644 --- a/lib/process-services-cloud/src/lib/people/services/identity-user.service.spec.ts +++ b/lib/process-services-cloud/src/lib/people/services/identity-user.service.spec.ts @@ -17,9 +17,7 @@ import { TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { JwtHelperService } from '@alfresco/adf-core'; import { IdentityUserService } from './identity-user.service'; -import { mockToken } from '../mock/jwt-helper.service.spec'; import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module'; import { mockSearchUserByApp, @@ -52,37 +50,6 @@ describe('IdentityUserService', () => { requestSpy = spyOn(adfHttpClient, 'request'); }); - describe('Current user info (JWT token)', () => { - - beforeEach(() => { - const store = {}; - - spyOn(localStorage, 'getItem').and.callFake((key: string): string => store[key] || null); - spyOn(localStorage, 'setItem').and.callFake((key: string, value: string): string => store[key] = value); - }); - - it('should fetch identity user info from Jwt id token', () => { - localStorage.setItem(JwtHelperService.USER_ID_TOKEN, mockToken); - const user = service.getCurrentUserInfo(); - expect(user).toBeDefined(); - expect(user.firstName).toEqual('John'); - expect(user.lastName).toEqual('Doe'); - expect(user.email).toEqual('johnDoe@gmail.com'); - expect(user.username).toEqual('johnDoe1'); - }); - - it('should fallback on Jwt access token for identity user info', () => { - localStorage.setItem(JwtHelperService.USER_ACCESS_TOKEN, mockToken); - const user = service.getCurrentUserInfo(); - expect(user).toBeDefined(); - expect(user.firstName).toEqual('John'); - expect(user.lastName).toEqual('Doe'); - expect(user.email).toEqual('johnDoe@gmail.com'); - expect(user.username).toEqual('johnDoe1'); - }); - - }); - describe('Search', () => { it('should fetch users', (done) => { diff --git a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts index 4ad3e3819d6..8633dde78f8 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts @@ -35,6 +35,7 @@ import { NotificationCloudService } from '../../../services/notification-cloud.s import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { IdentityUserService } from '../../../people/services/identity-user.service'; import { ApolloModule } from 'apollo-angular'; +import { StorageService } from '@alfresco/adf-core'; describe('TaskFilterCloudService', () => { let service: TaskFilterCloudService; @@ -242,12 +243,13 @@ describe('Inject [LocalPreferenceCloudService] into the TaskFilterCloudService', let preferenceCloudService: PreferenceCloudServiceInterface; let identityUserService: IdentityUserService; let getPreferencesSpy: jasmine.Spy; + let storageService: StorageService; const identityUserMock = { username: 'fakeusername', firstName: 'fake-identity-first-name', lastName: 'fake-identity-last-name', email: 'fakeIdentity@email.com' }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, ApolloModule], + imports: [HttpClientTestingModule, ProcessServiceCloudTestingModule, ApolloModule], providers: [ { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService } ] @@ -255,6 +257,7 @@ describe('Inject [LocalPreferenceCloudService] into the TaskFilterCloudService', service = TestBed.inject(TaskFilterCloudService); preferenceCloudService = service.preferenceService; identityUserService = TestBed.inject(IdentityUserService); + storageService = TestBed.inject(StorageService); getPreferencesSpy = spyOn(preferenceCloudService, 'getPreferences').and.returnValue(of([])); spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue(identityUserMock); }); @@ -285,7 +288,7 @@ describe('Inject [LocalPreferenceCloudService] into the TaskFilterCloudService', expect(res[2].status).toEqual('COMPLETED'); expect(getPreferencesSpy).toHaveBeenCalled(); - const localData = JSON.parse(localStorage.getItem(`task-filters-${appName}-${identityUserMock.username}`)); + const localData = JSON.parse(storageService.getItem(`task-filters-${appName}-${identityUserMock.username}`)); expect(localData.length).toEqual(3); expect(localData[0].name).toEqual('ADF_CLOUD_TASK_FILTERS.MY_TASKS'); diff --git a/lib/process-services-cloud/src/lib/testing/process-service-cloud.testing.module.ts b/lib/process-services-cloud/src/lib/testing/process-service-cloud.testing.module.ts index dbc4b359927..ff2ccedeb2e 100644 --- a/lib/process-services-cloud/src/lib/testing/process-service-cloud.testing.module.ts +++ b/lib/process-services-cloud/src/lib/testing/process-service-cloud.testing.module.ts @@ -25,7 +25,7 @@ import { AppConfigServiceMock, TranslationService, TranslationMock, - CoreModule + CoreModule, AuthModule } from '@alfresco/adf-core'; import { TranslateModule } from '@ngx-translate/core'; import { ProcessServicesCloudModule } from '../process-services-cloud.module'; @@ -33,6 +33,7 @@ import { RouterTestingModule } from '@angular/router/testing'; @NgModule({ imports: [ + AuthModule.forRoot({ useHash: true }), HttpClientModule, NoopAnimationsModule, RouterTestingModule, diff --git a/lib/process-services-cloud/src/lib/testing/process-services-cloud-story.module.ts b/lib/process-services-cloud/src/lib/testing/process-services-cloud-story.module.ts index b95dc72f36c..92f7f07648b 100644 --- a/lib/process-services-cloud/src/lib/testing/process-services-cloud-story.module.ts +++ b/lib/process-services-cloud/src/lib/testing/process-services-cloud-story.module.ts @@ -16,13 +16,14 @@ */ import { NgModule } from '@angular/core'; -import { CoreModule, provideTranslations } from '@alfresco/adf-core'; +import { AuthModule, CoreModule, provideTranslations } from '@alfresco/adf-core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { ProcessServicesCloudModule } from '../process-services-cloud.module'; @NgModule({ imports: [ + AuthModule.forRoot(), BrowserAnimationsModule, TranslateModule.forRoot(), CoreModule.forRoot(), diff --git a/lib/process-services/src/lib/form/widgets/content-widget/attach-file-widget-dialog.component.spec.ts b/lib/process-services/src/lib/form/widgets/content-widget/attach-file-widget-dialog.component.spec.ts index 664148ac19d..d7a2270f114 100644 --- a/lib/process-services/src/lib/form/widgets/content-widget/attach-file-widget-dialog.component.spec.ts +++ b/lib/process-services/src/lib/form/widgets/content-widget/attach-file-widget-dialog.component.spec.ts @@ -21,12 +21,13 @@ import { ContentModule, ContentNodeSelectorPanelComponent, DocumentListService, import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { ProcessTestingModule } from '../../../testing/process.testing.module'; import { AttachFileWidgetDialogComponent } from './attach-file-widget-dialog.component'; -import { AuthenticationService, AlfrescoApiService } from '@alfresco/adf-core'; +import { AlfrescoApiService, BasicAlfrescoAuthService } from '@alfresco/adf-core'; import { AttachFileWidgetDialogComponentData } from './attach-file-widget-dialog-component.interface'; import { of, throwError } from 'rxjs'; import { By } from '@angular/platform-browser'; import { Node, SiteEntry, NodeEntry, SitePaging, SitePagingList } from '@alfresco/js-api'; import { TranslateModule } from '@ngx-translate/core'; +import { OidcAuthenticationService } from 'lib/core/src/lib/auth/services/oidc-authentication.service'; describe('AttachFileWidgetDialogComponent', () => { @@ -40,7 +41,7 @@ describe('AttachFileWidgetDialogComponent', () => { ecmHost: 'http://fakeUrl.com' }; let element: HTMLInputElement; - let authService: AuthenticationService; + let basicAlfrescoAuthService: BasicAlfrescoAuthService; let siteService: SitesService; let nodeService: NodesApiService; let documentListService: DocumentListService; @@ -58,6 +59,7 @@ describe('AttachFileWidgetDialogComponent', () => { ProcessTestingModule ], providers: [ + { provide: OidcAuthenticationService, useValue: {} }, { provide: MAT_DIALOG_DATA, useValue: data }, { provide: MatDialogRef, useValue: { close: () => of() } } ], @@ -66,7 +68,7 @@ describe('AttachFileWidgetDialogComponent', () => { fixture = TestBed.createComponent(AttachFileWidgetDialogComponent); widget = fixture.componentInstance; element = fixture.nativeElement; - authService = fixture.debugElement.injector.get(AuthenticationService); + basicAlfrescoAuthService = fixture.debugElement.injector.get(BasicAlfrescoAuthService); siteService = fixture.debugElement.injector.get(SitesService); nodeService = fixture.debugElement.injector.get(NodesApiService); documentListService = fixture.debugElement.injector.get(DocumentListService); @@ -106,7 +108,7 @@ describe('AttachFileWidgetDialogComponent', () => { }); it('should be able to login', (done) => { - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket'})); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket'})); isLogged = true; let loginButton: HTMLButtonElement = element.querySelector('button[data-automation-id="attach-file-dialog-actions-login"]'); const usernameInput: HTMLInputElement = element.querySelector('#username'); @@ -173,7 +175,7 @@ describe('AttachFileWidgetDialogComponent', () => { describe('login only', () => { beforeEach(() => { - spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket'})); + spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket'})); spyOn(matDialogRef, 'close').and.callThrough(); fixture.detectChanges(); widget.data.loginOnly = true; @@ -192,7 +194,8 @@ describe('AttachFileWidgetDialogComponent', () => { usernameInput.dispatchEvent(new Event('input')); passwordInput.dispatchEvent(new Event('input')); loginButton.click(); - authService.onLogin.next('logged In'); + + basicAlfrescoAuthService.onLogin.next('logged-in'); fixture.detectChanges(); expect(matDialogRef.close).toHaveBeenCalled(); }); diff --git a/lib/process-services/src/lib/testing/process.testing.module.ts b/lib/process-services/src/lib/testing/process.testing.module.ts index 4a44138268d..cf1425926ef 100644 --- a/lib/process-services/src/lib/testing/process.testing.module.ts +++ b/lib/process-services/src/lib/testing/process.testing.module.ts @@ -26,7 +26,7 @@ import { TranslationService, TranslationMock, CoreModule, - FormRenderingService + FormRenderingService, AuthModule } from '@alfresco/adf-core'; import { TranslateModule } from '@ngx-translate/core'; import { ProcessFormRenderingService } from '../form/process-form-rendering.service'; @@ -34,6 +34,7 @@ import { RouterTestingModule } from '@angular/router/testing'; @NgModule({ imports: [ + AuthModule.forRoot({ useHash: true }), NoopAnimationsModule, TranslateModule.forRoot(), CoreModule.forRoot(), diff --git a/lib/testing/src/lib/protractor/content-services/actions/upload.actions.ts b/lib/testing/src/lib/protractor/content-services/actions/upload.actions.ts index 719cdac032d..e896db30034 100644 --- a/lib/testing/src/lib/protractor/content-services/actions/upload.actions.ts +++ b/lib/testing/src/lib/protractor/content-services/actions/upload.actions.ts @@ -37,7 +37,7 @@ export class UploadActions { async uploadFile(fileLocation: fs.PathLike, fileName: string, parentFolderId: string): Promise { const file = fs.createReadStream(fileLocation); - return this.uploadApi.uploadFile( + const uploadPromise = this.uploadApi.uploadFile( file, '', parentFolderId, @@ -48,6 +48,12 @@ export class UploadActions { renditions: 'doclib' } ); + + await uploadPromise.then(() => { + Logger.info(`${fileName} uploaded in ${parentFolderId}`); + }); + + return uploadPromise; } async createEmptyFiles(emptyFileNames: string[], parentFolderId: string): Promise { @@ -74,6 +80,7 @@ export class UploadActions { async deleteFileOrFolder(nodeId: string) { const apiCall = async () => { try { + Logger.info(`Deleting ${nodeId}`); return this.nodesApi.deleteNode(nodeId, { permanent: true }); } catch (error) { Logger.error('Error delete file or folder'); @@ -91,8 +98,18 @@ export class UploadActions { if (files && files.length > 0) { for (const fileName of files) { const pathFile = path.join(sourcePath, fileName); - promises.push(this.uploadFile(pathFile, fileName, folder)); + + const uploadPromise = this.uploadFile(pathFile, fileName, folder); + + await uploadPromise.then(() => { + Logger.info(`File ${fileName} uploaded successfully in ${folder}!`); + }).catch(() => { + Logger.error(`File ${fileName} error during the upload in ${folder}!`); + }); + + promises.push(uploadPromise); } + uploadedFiles = await Promise.all(promises); } diff --git a/lib/testing/src/lib/protractor/core/actions/identity/identity.service.ts b/lib/testing/src/lib/protractor/core/actions/identity/identity.service.ts index d07d49a87c8..beebe29f2b7 100644 --- a/lib/testing/src/lib/protractor/core/actions/identity/identity.service.ts +++ b/lib/testing/src/lib/protractor/core/actions/identity/identity.service.ts @@ -82,7 +82,14 @@ export class IdentityService { const method = 'DELETE'; const queryParams = {}; const postBody = {}; - return this.api.performIdentityOperation(path, method, queryParams, postBody); + + const deletePromise = this.api.performIdentityOperation(path, method, queryParams, postBody); + + await deletePromise.then(() => { + Logger.info(`user ${userId} delete`); + }); + + return deletePromise; } async getUserInfoByUsername(username: string): Promise { diff --git a/lib/testing/src/lib/protractor/core/pages/login.page.ts b/lib/testing/src/lib/protractor/core/pages/login.page.ts index 598345bb0ed..62bd09fc6b6 100644 --- a/lib/testing/src/lib/protractor/core/pages/login.page.ts +++ b/lib/testing/src/lib/protractor/core/pages/login.page.ts @@ -66,6 +66,8 @@ export class LoginPage { Logger.log('Login With ' + username); const authType = await LocalStorageUtil.getConfigField('authType'); + Logger.log(`AuthType ${authType}`); + if (!authType || authType === 'OAUTH') { await this.loginSSOIdentityService(username, password, options); } else { @@ -83,7 +85,10 @@ export class LoginPage { await BrowserActions.getUrl(loginURL); if (oauth2 && oauth2.silentLogin === false) { + Logger.log(`Login SSO`); await this.clickOnSSOButton(); + }else{ + Logger.log(`Login SSO silent login`); } await BrowserVisibility.waitUntilElementIsVisible(this.usernameField); @@ -98,6 +103,8 @@ export class LoginPage { } async loginBasicAuth(username: string, password: string, options: LoginOptions = { waitForUserIcon: true }): Promise { + Logger.log(`Login Basic`); + await this.goToLoginPage(); await this.enterUsernameBasicAuth(username); diff --git a/lib/testing/src/lib/protractor/core/utils/file-browser.util.ts b/lib/testing/src/lib/protractor/core/utils/file-browser.util.ts index f02394ee0a4..3617fdc607f 100644 --- a/lib/testing/src/lib/protractor/core/utils/file-browser.util.ts +++ b/lib/testing/src/lib/protractor/core/utils/file-browser.util.ts @@ -23,7 +23,7 @@ export class FileBrowserUtil { static async isFileDownloaded(fileName: string): Promise { const DEFAULT_ROOT_PATH = browser.params.testConfig ? browser.params.testConfig.main.rootPath : __dirname; - const file = await browser.driver.wait(() => fs.existsSync(path.join(DEFAULT_ROOT_PATH, 'downloads', fileName)), 30000); + const file = await browser.driver.wait(() => fs.existsSync(path.join(DEFAULT_ROOT_PATH, 'downloads', fileName)), 30000,`${fileName} not downloaded`); await expect(file).toBe(true, `${fileName} not downloaded`); diff --git a/lib/testing/src/lib/protractor/core/utils/local-storage.util.ts b/lib/testing/src/lib/protractor/core/utils/local-storage.util.ts index 5f92a0e497a..e962b6918cf 100644 --- a/lib/testing/src/lib/protractor/core/utils/local-storage.util.ts +++ b/lib/testing/src/lib/protractor/core/utils/local-storage.util.ts @@ -17,6 +17,9 @@ import { browser } from 'protractor'; +/* +Open the CoreAutomationService in ADF core to see where we augment the window +*/ export class LocalStorageUtil { static async getConfigField(field: string): Promise { return browser.executeScript('return window.adf ? window.adf.getConfigField(`' + field + '`) : null;');