diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52f20470a3c..d71c031b930 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: #CHROME_VERSION: "90.0.4430.212-1" # Bump Node heap size (OOM in CI after upgrading to Angular 15) NODE_OPTIONS: '--max-old-space-size=4096' - # Project name to use when running docker-compose prior to e2e tests + # Project name to use when running "docker compose" prior to e2e tests COMPOSE_PROJECT_NAME: 'ci' strategy: # Create a matrix of Node versions to test against (in parallel) @@ -108,12 +108,12 @@ jobs: path: 'coverage/dspace-angular/lcov.info' retention-days: 14 - # Using docker-compose start backend using CI configuration + # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) run: | - docker-compose -f ./docker/docker-compose-ci.yml up -d - docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli + docker compose -f ./docker/docker-compose-ci.yml up -d + docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli docker container ls # Run integration tests via Cypress.io @@ -182,7 +182,7 @@ jobs: run: kill -9 $(lsof -t -i:4000) - name: Shutdown Docker containers - run: docker-compose -f ./docker/docker-compose-ci.yml down + run: docker compose -f ./docker/docker-compose-ci.yml down # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test # job above. This is necessary because Codecov uploads seem to randomly fail at times. diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index 0c8c64a4706..02de06f4155 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -53,6 +53,7 @@ import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { NoContent } from '../../../core/shared/NoContent.model'; import { PageInfo } from '../../../core/shared/page-info.model'; import { UUIDService } from '../../../core/shared/uuid.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { AlertComponent } from '../../../shared/alert/alert.component'; import { ContextHelpDirective } from '../../../shared/context-help.directive'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -244,6 +245,7 @@ describe('GroupFormComponent', () => { { provide: HttpClient, useValue: {} }, { provide: ObjectCacheService, useValue: {} }, { provide: UUIDService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 321d9469072..9ee767cda05 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -31,6 +31,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { HostWindowService } from '../../../shared/host-window.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; @@ -143,6 +144,7 @@ describe('BitstreamFormatsComponent', () => { { provide: PaginationService, useValue: paginationService }, { provide: GroupDataService, useValue: groupDataService }, { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts index fbb936080fd..cd722542f63 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts @@ -17,6 +17,7 @@ import { AuthorizationDataService } from '../../../../../core/data/feature-autho import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../../shared/mocks/auth.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; @@ -67,6 +68,7 @@ describe('WorkflowItemSearchResultAdminWorkflowListElementComponent', () => { { provide: ThemeService, useValue: getMockThemeService() }, { provide: AuthService, useValue: new AuthServiceMock() }, { provide: AuthorizationDataService, useValue: {} }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }) diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 76e70e8a6d1..92442854ba1 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -16,6 +16,7 @@ import { getTestScheduler, } from 'jasmine-marbles'; import { + BehaviorSubject, EMPTY, Observable, of as observableOf, @@ -32,6 +33,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { coreReducers } from '../core.reducers'; import { CoreState } from '../core-state.model'; import { UUIDService } from '../shared/uuid.service'; +import { XSRFService } from '../xsrf/xsrf.service'; import { RequestConfigureAction, RequestExecuteAction, @@ -59,6 +61,7 @@ describe('RequestService', () => { let uuidService: UUIDService; let store: Store; let mockStore: MockStore; + let xsrfService: XSRFService; const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'; const testHref = 'https://rest.api/endpoint/selfLink'; @@ -104,10 +107,15 @@ describe('RequestService', () => { store = TestBed.inject(Store); mockStore = store as MockStore; mockStore.setState(initialState); + xsrfService = { + tokenInitialized$: new BehaviorSubject(false), + } as XSRFService; + service = new RequestService( objectCache, uuidService, store, + xsrfService, undefined, ); serviceAsAny = service as any; diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 9bd262b1adb..6ce37d35450 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -42,6 +42,7 @@ import { requestIndexSelector, } from '../index/index.selectors'; import { UUIDService } from '../shared/uuid.service'; +import { XSRFService } from '../xsrf/xsrf.service'; import { RequestConfigureAction, RequestExecuteAction, @@ -168,6 +169,7 @@ export class RequestService { constructor(private objectCache: ObjectCacheService, private uuidService: UUIDService, private store: Store, + protected xsrfService: XSRFService, private indexStore: Store) { } @@ -450,7 +452,17 @@ export class RequestService { */ private dispatchRequest(request: RestRequest) { this.store.dispatch(new RequestConfigureAction(request)); - this.store.dispatch(new RequestExecuteAction(request.uuid)); + // If it's a GET request, or we have an XSRF token, dispatch it immediately + if (request.method === RestRequestMethod.GET || this.xsrfService.tokenInitialized$.getValue() === true) { + this.store.dispatch(new RequestExecuteAction(request.uuid)); + } else { + // Otherwise wait for the XSRF token first + this.xsrfService.tokenInitialized$.pipe( + find((hasInitialized: boolean) => hasInitialized === true), + ).subscribe(() => { + this.store.dispatch(new RequestExecuteAction(request.uuid)); + }); + } } /** diff --git a/src/app/core/xsrf/browser-xsrf.service.spec.ts b/src/app/core/xsrf/browser-xsrf.service.spec.ts new file mode 100644 index 00000000000..aba3edd3305 --- /dev/null +++ b/src/app/core/xsrf/browser-xsrf.service.spec.ts @@ -0,0 +1,58 @@ +import { HttpClient } from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; +import { BrowserXSRFService } from './browser-xsrf.service'; + +describe(`BrowserXSRFService`, () => { + let service: BrowserXSRFService; + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + + const endpointURL = new RESTURLCombiner('/security/csrf').toString(); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ], + providers: [ BrowserXSRFService ], + }); + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); + service = TestBed.inject(BrowserXSRFService); + }); + + describe(`initXSRFToken`, () => { + it(`should perform a GET to the csrf endpoint`, (done: DoneFn) => { + service.initXSRFToken(httpClient)(); + + const req = httpTestingController.expectOne({ + url: endpointURL, + method: 'GET', + }); + + req.flush({}); + httpTestingController.verify(); + expect().nothing(); + done(); + }); + + describe(`when the GET succeeds`, () => { + it(`should set tokenInitialized$ to true`, (done: DoneFn) => { + service.initXSRFToken(httpClient)(); + + const req = httpTestingController.expectOne(endpointURL); + + req.flush({}); + httpTestingController.verify(); + + expect(service.tokenInitialized$.getValue()).toBeTrue(); + done(); + }); + }); + + }); +}); diff --git a/src/app/core/xsrf/browser-xsrf.service.ts b/src/app/core/xsrf/browser-xsrf.service.ts new file mode 100644 index 00000000000..121defc061b --- /dev/null +++ b/src/app/core/xsrf/browser-xsrf.service.ts @@ -0,0 +1,30 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { take } from 'rxjs/operators'; + +import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; +import { XSRFService } from './xsrf.service'; + +/** + * Browser (CSR) Service to obtain a new CSRF/XSRF token when needed by our RequestService + * to perform a modify request (e.g. POST/PUT/DELETE). + * NOTE: This is primarily necessary before the *first* modifying request, as the CSRF + * token may not yet be initialized. + */ +@Injectable() +export class BrowserXSRFService extends XSRFService { + initXSRFToken(httpClient: HttpClient): () => Promise { + return () => new Promise((resolve) => { + // Force a new token to be created by calling the CSRF endpoint + httpClient.get(new RESTURLCombiner('/security/csrf').toString(), undefined).pipe( + take(1), + ).subscribe(() => { + // Once token is returned, set tokenInitialized to true. + this.tokenInitialized$.next(true); + }); + + // return immediately, the rest of the app doesn't need to wait for this to finish + resolve(); + }); + } +} diff --git a/src/app/core/xsrf/server-xsrf.service.spec.ts b/src/app/core/xsrf/server-xsrf.service.spec.ts new file mode 100644 index 00000000000..05728edb423 --- /dev/null +++ b/src/app/core/xsrf/server-xsrf.service.spec.ts @@ -0,0 +1,33 @@ +import { HttpClient } from '@angular/common/http'; + +import { ServerXSRFService } from './server-xsrf.service'; + +describe(`ServerXSRFService`, () => { + let service: ServerXSRFService; + let httpClient: HttpClient; + + beforeEach(() => { + httpClient = jasmine.createSpyObj(['post', 'get', 'request']); + service = new ServerXSRFService(); + }); + + describe(`initXSRFToken`, () => { + it(`shouldn't perform any requests`, (done: DoneFn) => { + service.initXSRFToken(httpClient)().then(() => { + for (const prop in httpClient) { + if (httpClient.hasOwnProperty(prop)) { + expect(httpClient[prop]).not.toHaveBeenCalled(); + } + } + done(); + }); + }); + + it(`should leave tokenInitialized$ on false`, (done: DoneFn) => { + service.initXSRFToken(httpClient)().then(() => { + expect(service.tokenInitialized$.getValue()).toBeFalse(); + done(); + }); + }); + }); +}); diff --git a/src/app/core/xsrf/server-xsrf.service.ts b/src/app/core/xsrf/server-xsrf.service.ts new file mode 100644 index 00000000000..f729aa49a7d --- /dev/null +++ b/src/app/core/xsrf/server-xsrf.service.ts @@ -0,0 +1,19 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { XSRFService } from './xsrf.service'; + +/** + * Server (SSR) Service to obtain a new CSRF/XSRF token. Because SSR only triggers GET + * requests a CSRF token is never needed. + */ +@Injectable() +export class ServerXSRFService extends XSRFService { + initXSRFToken(httpClient: HttpClient): () => Promise { + return () => new Promise((resolve) => { + // return immediately, and keep tokenInitialized$ false. The server side can make only GET + // requests, since it can never get a valid XSRF cookie + resolve(); + }); + } +} diff --git a/src/app/core/xsrf/xsrf.service.spec.ts b/src/app/core/xsrf/xsrf.service.spec.ts new file mode 100644 index 00000000000..56564a294ca --- /dev/null +++ b/src/app/core/xsrf/xsrf.service.spec.ts @@ -0,0 +1,21 @@ +import { HttpClient } from '@angular/common/http'; + +import { XSRFService } from './xsrf.service'; + +class XSRFServiceImpl extends XSRFService { + initXSRFToken(httpClient: HttpClient): () => Promise { + return () => null; + } +} + +describe(`XSRFService`, () => { + let service: XSRFService; + + beforeEach(() => { + service = new XSRFServiceImpl(); + }); + + it(`should start with tokenInitialized$.hasValue() === false`, () => { + expect(service.tokenInitialized$.getValue()).toBeFalse(); + }); +}); diff --git a/src/app/core/xsrf/xsrf.service.ts b/src/app/core/xsrf/xsrf.service.ts new file mode 100644 index 00000000000..99b27021b66 --- /dev/null +++ b/src/app/core/xsrf/xsrf.service.ts @@ -0,0 +1,15 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +/** + * Abstract CSRF/XSRF Service used to track whether a CSRF token has been received + * from the DSpace REST API. Once it is received, the "tokenInitialized$" flag will + * be set to "true". + */ +@Injectable() +export abstract class XSRFService { + public tokenInitialized$: BehaviorSubject = new BehaviorSubject(false); + + abstract initXSRFToken(httpClient: HttpClient): () => Promise; +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts index b041215c9cb..b3c9ab1d9bf 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts @@ -35,6 +35,7 @@ import { Bitstream } from '../../../../../core/shared/bitstream.model'; import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service'; import { Item } from '../../../../../core/shared/item.model'; import { UUIDService } from '../../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; @@ -138,6 +139,7 @@ describe('PersonSearchResultListElementSubmissionComponent', () => { { provide: Store, useValue: {} }, { provide: ObjectCacheService, useValue: {} }, { provide: UUIDService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: CommunityDataService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 0396f23b6d0..fd6a8fe0509 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -38,6 +38,7 @@ import { ItemType } from '../../../../core/shared/item-relationships/item-type.m import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { HostWindowService } from '../../../../shared/host-window.service'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; @@ -257,6 +258,7 @@ describe('EditRelationshipListComponent', () => { { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: AuthRequestService, useValue: new AuthRequestServiceStub() }, { provide: HardRedirectService, useValue: hardRedirectService }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environmentUseThumbs }, { provide: REQUEST, useValue: {} }, CookieService, diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts index 91eb255afd7..b2fb2bf29f0 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts @@ -22,6 +22,7 @@ import { import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; @@ -83,6 +84,7 @@ describe('FileSectionComponent', () => { }), BrowserAnimationsModule, FileSectionComponent, VarDirective, FileSizePipe], providers: [ { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: APP_CONFIG, useValue: environment }, diff --git a/src/app/login-page/login-page.component.spec.ts b/src/app/login-page/login-page.component.spec.ts index 74aeddfe0cf..6cb4098c4de 100644 --- a/src/app/login-page/login-page.component.spec.ts +++ b/src/app/login-page/login-page.component.spec.ts @@ -12,6 +12,7 @@ import { of as observableOf } from 'rxjs'; import { APP_DATA_SERVICES_MAP } from '../../config/app-config.interface'; import { AuthService } from '../core/auth/auth.service'; +import { XSRFService } from '../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../shared/mocks/auth.service.mock'; import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; import { LoginPageComponent } from './login-page.component'; @@ -39,6 +40,7 @@ describe('LoginPageComponent', () => { providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: XSRFService, useValue: {} }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, provideMockStore({}), ], diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index 224620c6e1f..a969b6f49bc 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -27,6 +27,7 @@ import { } from '../../core/auth/auth.reducer'; import { AuthService } from '../../core/auth/auth.service'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { HostWindowService } from '../host-window.service'; import { ActivatedRouteStub } from '../testing/active-router.stub'; import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; @@ -102,6 +103,7 @@ describe('AuthNavMenuComponent', () => { { provide: HostWindowService, useValue: window }, { provide: AuthService, useValue: authService }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: XSRFService, useValue: {} }, ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts index d57db27684f..c7ddf2ac34a 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts @@ -29,6 +29,7 @@ import { } from '../../../core/auth/auth.reducer'; import { AuthService } from '../../../core/auth/auth.service'; import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { TranslateLoaderMock } from '../../mocks/translate-loader.mock'; import { ActivatedRouteStub } from '../../testing/active-router.stub'; import { EPersonMock } from '../../testing/eperson.mock'; @@ -91,6 +92,7 @@ describe('UserMenuComponent', () => { providers: [ { provide: AuthService, useValue: authService }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: XSRFService, useValue: {} }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ], schemas: [ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts index 535b88561a4..05da3cc5833 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts @@ -43,6 +43,7 @@ import { FormRowModel } from '../../../../../../core/config/models/config-submis import { SubmissionFormsModel } from '../../../../../../core/config/models/config-submission-forms.model'; import { SubmissionObjectDataService } from '../../../../../../core/submission/submission-object-data.service'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { XSRFService } from '../../../../../../core/xsrf/xsrf.service'; import { SubmissionService } from '../../../../../../submission/submission.service'; import { createTestComponent } from '../../../../../testing/utils.test'; import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; @@ -180,6 +181,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { { provide: DsDynamicTypeBindRelationService, useClass: DsDynamicTypeBindRelationService }, { provide: SubmissionObjectDataService, useValue: {} }, { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts index 4995b36f116..94de9eaaa7b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts @@ -33,6 +33,7 @@ import { ExternalSource } from '../../../../../core/shared/external-source.model import { Item } from '../../../../../core/shared/item.model'; import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; @@ -147,6 +148,7 @@ describe('DsDynamicLookupRelationModalComponent', () => { }, }, }, + { provide: XSRFService, useValue: {} }, { provide: NgZone, useValue: new NgZone({}) }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, NgbActiveModal, diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index f88d63f4c24..431ae3387f1 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -33,6 +33,7 @@ import { BehaviorSubject } from 'rxjs'; import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface'; import { storeModuleConfig } from '../../app.reducer'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { StoreMock } from '../testing/store.mock'; import { createTestComponent } from '../testing/utils.test'; import { DsDynamicFormComponent } from './builder/ds-dynamic-form-ui/ds-dynamic-form.component'; @@ -176,6 +177,7 @@ describe('FormComponent test suite', () => { FormComponent, FormService, { provide: Store, useClass: StoreMock }, + { provide: XSRFService, useValue: {} }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts index 495450be75e..82d44cb58c8 100644 --- a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts +++ b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts @@ -18,6 +18,7 @@ import { of } from 'rxjs'; import { AuthRequestService } from 'src/app/core/auth/auth-request.service'; import { CookieService } from 'src/app/core/services/cookie.service'; import { HardRedirectService } from 'src/app/core/services/hard-redirect.service'; +import { XSRFService } from 'src/app/core/xsrf/xsrf.service'; import { CookieServiceMock } from 'src/app/shared/mocks/cookie.service.mock'; import { getMockThemeService } from 'src/app/shared/mocks/theme-service.mock'; import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub'; @@ -70,6 +71,7 @@ describe('ListableObjectComponentLoaderComponent', () => { { provide: HardRedirectService, useValue: jasmine.createSpyObj('hardRedirectService', ['redirect']) }, { provide: AuthRequestService, useValue: new AuthRequestServiceStub() }, { provide: CookieService, useValue: new CookieServiceMock() }, + { provide: XSRFService, useValue: {} }, { provide: REQUEST, useValue: {} }, { provide: ActivatedRoute, diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts index b7eb800764f..77225f3ed5e 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts @@ -32,6 +32,7 @@ import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service import { Item } from '../../../../core/shared/item.model'; import { SearchService } from '../../../../core/shared/search/search.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../shared/mocks/auth.service.mock'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; import { SearchServiceStub } from '../../../../shared/testing/search-service.stub'; @@ -114,6 +115,7 @@ describe('ItemDetailPreviewComponent', () => { { provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') }, { provide: ObjectCacheService, useValue: {} }, { provide: UUIDService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: CommunityDataService, useValue: {} }, diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts index f8a40249b09..bfec6ebeac7 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts @@ -26,6 +26,7 @@ import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.ser import { Collection } from '../../../../core/shared/collection.model'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { NotificationsService } from '../../../notifications/notifications.service'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { ActivatedRouteStub } from '../../../testing/active-router.stub'; @@ -95,6 +96,7 @@ describe('CollectionSearchResultGridElementComponent', () => { { provide: DSOChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, { provide: BitstreamFormatDataService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: LinkService, useValue: linkService }, provideMockStore({}), ], diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts index f60d0c5454e..86d757a0308 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts @@ -26,6 +26,7 @@ import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.ser import { Community } from '../../../../core/shared/community.model'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../shared/mocks/auth.service.mock'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; import { StoreMock } from '../../../../shared/testing/store.mock'; @@ -100,6 +101,7 @@ describe('CommunitySearchResultGridElementComponent', () => { { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: ThemeService, useValue: getMockThemeService() }, { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(CommunitySearchResultGridElementComponent, { diff --git a/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts b/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts index 597b3cfae1a..47a8aa65028 100644 --- a/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts +++ b/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts @@ -17,6 +17,7 @@ import { AuthService } from '../../../../../core/auth/auth.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service'; import { Item } from '../../../../../core/shared/item.model'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../../shared/mocks/auth.service.mock'; import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; @@ -78,6 +79,7 @@ describe('ItemListElementComponent', () => { { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: AuthService, useValue: new AuthServiceMock() }, { provide: AuthorizationDataService, useValue: {} }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(ItemListElementComponent, { diff --git a/src/app/shared/search/search.component.spec.ts b/src/app/shared/search/search.component.spec.ts index fec23281cdb..8c7b5b06ff6 100644 --- a/src/app/shared/search/search.component.spec.ts +++ b/src/app/shared/search/search.component.spec.ts @@ -46,6 +46,7 @@ import { SearchConfig, SortConfig, } from '../../core/shared/search/search-filters/search-config.model'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-configuration.service'; import { HostWindowService } from '../host-window.service'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; @@ -237,6 +238,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar provide: SearchFilterService, useValue: {}, }, + { provide: XSRFService, useValue: {} }, { provide: SEARCH_CONFIG_SERVICE, useValue: searchConfigurationServiceStub, diff --git a/src/app/submission/edit/submission-edit.component.spec.ts b/src/app/submission/edit/submission-edit.component.spec.ts index ffac7959ebc..81c5c1ddeac 100644 --- a/src/app/submission/edit/submission-edit.component.spec.ts +++ b/src/app/submission/edit/submission-edit.component.spec.ts @@ -22,6 +22,7 @@ import { AuthService } from '../../core/auth/auth.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { mockSubmissionObject } from '../../shared/mocks/submission.mock'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -83,6 +84,7 @@ describe('SubmissionEditComponent Component', () => { { provide: HALEndpointService, useValue: halService }, { provide: SectionsService, useValue: new SectionsServiceStub() }, { provide: ThemeService, useValue: themeService }, + { provide: XSRFService, useValue: {} }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, provideMockStore(), ], diff --git a/src/app/submission/sections/accesses/section-accesses.component.spec.ts b/src/app/submission/sections/accesses/section-accesses.component.spec.ts index 68287149505..c49bd74e0d6 100644 --- a/src/app/submission/sections/accesses/section-accesses.component.spec.ts +++ b/src/app/submission/sections/accesses/section-accesses.component.spec.ts @@ -24,6 +24,7 @@ import { SubmissionAccessesConfigDataService } from '../../../core/config/submis import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { dsDynamicFormControlMapFn } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn'; import { DsDynamicTypeBindRelationService } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -120,6 +121,7 @@ describe('SubmissionSectionAccessesComponent', () => { { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: SubmissionObjectDataService, useValue: {} }, { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, @@ -216,6 +218,7 @@ describe('SubmissionSectionAccessesComponent', () => { { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: SubmissionObjectDataService, useValue: {} }, { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, diff --git a/src/app/submission/sections/license/section-license.component.spec.ts b/src/app/submission/sections/license/section-license.component.spec.ts index c3cb329a504..95b2e7f50ab 100644 --- a/src/app/submission/sections/license/section-license.component.spec.ts +++ b/src/app/submission/sections/license/section-license.component.spec.ts @@ -38,6 +38,7 @@ import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/jso import { Collection } from '../../../core/shared/collection.model'; import { License } from '../../../core/shared/license.model'; import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { dsDynamicFormControlMapFn } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; @@ -191,6 +192,7 @@ describe('SubmissionSectionLicenseComponent test suite', () => { findById: () => observableOf(createSuccessfulRemoteDataObject(mockSubmissionObject)), }, }, + { provide: XSRFService, useValue: {} }, SubmissionSectionLicenseComponent, ], schemas: [NO_ERRORS_SCHEMA], diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts index 8543ee40891..015ccd4ae41 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts @@ -38,6 +38,7 @@ import { environment } from '../../../../../../environments/environment.test'; import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder'; import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { dateToISOFormat } from '../../../../../shared/date.util'; import { DsDynamicTypeBindRelationService } from '../../../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { DynamicCustomSwitchModel } from '../../../../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; @@ -154,6 +155,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index cb8431beb8c..97e6e0e8603 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -2,7 +2,10 @@ import { HttpClient, HttpClientModule, } from '@angular/common/http'; -import { NgModule } from '@angular/core'; +import { + APP_INITIALIZER, + NgModule, +} from '@angular/core'; import { BrowserModule, BrowserTransferStateModule, @@ -48,6 +51,8 @@ import { ClientCookieService } from '../../app/core/services/client-cookie.servi import { CookieService } from '../../app/core/services/cookie.service'; import { HardRedirectService } from '../../app/core/services/hard-redirect.service'; import { ReferrerService } from '../../app/core/services/referrer.service'; +import { BrowserXSRFService } from '../../app/core/xsrf/browser-xsrf.service'; +import { XSRFService } from '../../app/core/xsrf/xsrf.service'; import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper'; @@ -98,6 +103,16 @@ export function getRequest(transferState: TransferState): any { useFactory: getRequest, deps: [TransferState], }, + { + provide: APP_INITIALIZER, + useFactory: (xsrfService: XSRFService, httpClient: HttpClient) => xsrfService.initXSRFToken(httpClient), + deps: [ XSRFService, HttpClient ], + multi: true, + }, + { + provide: XSRFService, + useClass: BrowserXSRFService, + }, { provide: AuthService, useClass: AuthService, diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 4de5687d041..20db68c6711 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -46,6 +46,8 @@ import { ServerReferrerService } from '../../app/core/services/server.referrer.s import { ServerCookieService } from '../../app/core/services/server-cookie.service'; import { ServerHardRedirectService } from '../../app/core/services/server-hard-redirect.service'; import { ServerXhrService } from '../../app/core/services/server-xhr.service'; +import { ServerXSRFService } from '../../app/core/xsrf/server-xsrf.service'; +import { XSRFService } from '../../app/core/xsrf/xsrf.service'; import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock'; import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock'; import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; @@ -112,6 +114,10 @@ export function createTranslateLoader(transferState: TransferState) { provide: AuthRequestService, useClass: ServerAuthRequestService, }, + { + provide: XSRFService, + useClass: ServerXSRFService, + }, { provide: LocaleService, useClass: ServerLocaleService,