Skip to content

Commit 32f1d10

Browse files
committed
Merge branch 'main' into DURACOM-234
# Conflicts: # src/app/core/data/request.service.ts # src/modules/app/browser-app.config.ts # src/modules/app/server-app.config.ts
2 parents 35714b7 + 86ddd45 commit 32f1d10

35 files changed

Lines changed: 284 additions & 11 deletions

File tree

.github/workflows/build.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
#CHROME_VERSION: "90.0.4430.212-1"
3434
# Bump Node heap size (OOM in CI after upgrading to Angular 15)
3535
NODE_OPTIONS: '--max-old-space-size=4096'
36-
# Project name to use when running docker-compose prior to e2e tests
36+
# Project name to use when running "docker compose" prior to e2e tests
3737
COMPOSE_PROJECT_NAME: 'ci'
3838
strategy:
3939
# Create a matrix of Node versions to test against (in parallel)
@@ -108,12 +108,12 @@ jobs:
108108
path: 'coverage/dspace-angular/lcov.info'
109109
retention-days: 14
110110

111-
# Using docker-compose start backend using CI configuration
111+
# Using "docker compose" start backend using CI configuration
112112
# and load assetstore from a cached copy
113113
- name: Start DSpace REST Backend via Docker (for e2e tests)
114114
run: |
115-
docker-compose -f ./docker/docker-compose-ci.yml up -d
116-
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
115+
docker compose -f ./docker/docker-compose-ci.yml up -d
116+
docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
117117
docker container ls
118118
119119
# Run integration tests via Cypress.io
@@ -182,7 +182,7 @@ jobs:
182182
run: kill -9 $(lsof -t -i:4000)
183183

184184
- name: Shutdown Docker containers
185-
run: docker-compose -f ./docker/docker-compose-ci.yml down
185+
run: docker compose -f ./docker/docker-compose-ci.yml down
186186

187187
# Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test
188188
# job above. This is necessary because Codecov uploads seem to randomly fail at times.

src/app/access-control/group-registry/group-form/group-form.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
5353
import { NoContent } from '../../../core/shared/NoContent.model';
5454
import { PageInfo } from '../../../core/shared/page-info.model';
5555
import { UUIDService } from '../../../core/shared/uuid.service';
56+
import { XSRFService } from '../../../core/xsrf/xsrf.service';
5657
import { AlertComponent } from '../../../shared/alert/alert.component';
5758
import { ContextHelpDirective } from '../../../shared/context-help.directive';
5859
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
@@ -244,6 +245,7 @@ describe('GroupFormComponent', () => {
244245
{ provide: HttpClient, useValue: {} },
245246
{ provide: ObjectCacheService, useValue: {} },
246247
{ provide: UUIDService, useValue: {} },
248+
{ provide: XSRFService, useValue: {} },
247249
{ provide: Store, useValue: {} },
248250
{ provide: RemoteDataBuildService, useValue: {} },
249251
{ provide: HALEndpointService, useValue: {} },

src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
3131
import { PaginationService } from '../../../core/pagination/pagination.service';
3232
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
3333
import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level';
34+
import { XSRFService } from '../../../core/xsrf/xsrf.service';
3435
import { HostWindowService } from '../../../shared/host-window.service';
3536
import { NotificationsService } from '../../../shared/notifications/notifications.service';
3637
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
@@ -143,6 +144,7 @@ describe('BitstreamFormatsComponent', () => {
143144
{ provide: PaginationService, useValue: paginationService },
144145
{ provide: GroupDataService, useValue: groupDataService },
145146
{ provide: ConfigurationDataService, useValue: configurationDataService },
147+
{ provide: XSRFService, useValue: {} },
146148
],
147149
schemas: [NO_ERRORS_SCHEMA],
148150
}).compileComponents();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { AuthorizationDataService } from '../../../../../core/data/feature-autho
1717
import { Item } from '../../../../../core/shared/item.model';
1818
import { ViewMode } from '../../../../../core/shared/view-mode.model';
1919
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
20+
import { XSRFService } from '../../../../../core/xsrf/xsrf.service';
2021
import { AuthServiceMock } from '../../../../../shared/mocks/auth.service.mock';
2122
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
2223
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
@@ -67,6 +68,7 @@ describe('WorkflowItemSearchResultAdminWorkflowListElementComponent', () => {
6768
{ provide: ThemeService, useValue: getMockThemeService() },
6869
{ provide: AuthService, useValue: new AuthServiceMock() },
6970
{ provide: AuthorizationDataService, useValue: {} },
71+
{ provide: XSRFService, useValue: {} },
7072
],
7173
schemas: [NO_ERRORS_SCHEMA],
7274
})

src/app/core/data/request.service.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
getTestScheduler,
1818
} from 'jasmine-marbles';
1919
import {
20+
BehaviorSubject,
2021
EMPTY,
2122
Observable,
2223
of as observableOf,
@@ -33,6 +34,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
3334
import { coreReducers } from '../core.reducers';
3435
import { CoreState } from '../core-state.model';
3536
import { UUIDService } from '../shared/uuid.service';
37+
import { XSRFService } from '../xsrf/xsrf.service';
3638
import {
3739
RequestConfigureAction,
3840
RequestExecuteAction,
@@ -60,6 +62,7 @@ describe('RequestService', () => {
6062
let uuidService: UUIDService;
6163
let store: Store<CoreState>;
6264
let mockStore: MockStore<CoreState>;
65+
let xsrfService: XSRFService;
6366

6467
const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb';
6568
const testHref = 'https://rest.api/endpoint/selfLink';
@@ -105,10 +108,15 @@ describe('RequestService', () => {
105108
store = TestBed.inject(Store);
106109
mockStore = store as MockStore<CoreState>;
107110
mockStore.setState(initialState);
111+
xsrfService = {
112+
tokenInitialized$: new BehaviorSubject(false),
113+
} as XSRFService;
114+
108115
service = new RequestService(
109116
objectCache,
110117
uuidService,
111118
store,
119+
xsrfService,
112120
undefined,
113121
);
114122
serviceAsAny = service as any;

src/app/core/data/request.service.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
requestIndexSelector,
4444
} from '../index/index.selectors';
4545
import { UUIDService } from '../shared/uuid.service';
46+
import { XSRFService } from '../xsrf/xsrf.service';
4647
import {
4748
RequestConfigureAction,
4849
RequestExecuteAction,
@@ -169,6 +170,7 @@ export class RequestService {
169170
constructor(private objectCache: ObjectCacheService,
170171
private uuidService: UUIDService,
171172
private store: Store<CoreState>,
173+
protected xsrfService: XSRFService,
172174
private indexStore: Store<MetaIndexState>) {
173175
}
174176

@@ -453,7 +455,17 @@ export class RequestService {
453455
private dispatchRequest(request: RestRequest) {
454456
asapScheduler.schedule(() => {
455457
this.store.dispatch(new RequestConfigureAction(request));
456-
this.store.dispatch(new RequestExecuteAction(request.uuid));
458+
// If it's a GET request, or we have an XSRF token, dispatch it immediately
459+
if (request.method === RestRequestMethod.GET || this.xsrfService.tokenInitialized$.getValue() === true) {
460+
this.store.dispatch(new RequestExecuteAction(request.uuid));
461+
} else {
462+
// Otherwise wait for the XSRF token first
463+
this.xsrfService.tokenInitialized$.pipe(
464+
find((hasInitialized: boolean) => hasInitialized === true),
465+
).subscribe(() => {
466+
this.store.dispatch(new RequestExecuteAction(request.uuid));
467+
});
468+
}
457469
});
458470
}
459471

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import {
3+
HttpClientTestingModule,
4+
HttpTestingController,
5+
} from '@angular/common/http/testing';
6+
import { TestBed } from '@angular/core/testing';
7+
8+
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
9+
import { BrowserXSRFService } from './browser-xsrf.service';
10+
11+
describe(`BrowserXSRFService`, () => {
12+
let service: BrowserXSRFService;
13+
let httpClient: HttpClient;
14+
let httpTestingController: HttpTestingController;
15+
16+
const endpointURL = new RESTURLCombiner('/security/csrf').toString();
17+
18+
beforeEach(() => {
19+
TestBed.configureTestingModule({
20+
imports: [ HttpClientTestingModule ],
21+
providers: [ BrowserXSRFService ],
22+
});
23+
httpClient = TestBed.inject(HttpClient);
24+
httpTestingController = TestBed.inject(HttpTestingController);
25+
service = TestBed.inject(BrowserXSRFService);
26+
});
27+
28+
describe(`initXSRFToken`, () => {
29+
it(`should perform a GET to the csrf endpoint`, (done: DoneFn) => {
30+
service.initXSRFToken(httpClient)();
31+
32+
const req = httpTestingController.expectOne({
33+
url: endpointURL,
34+
method: 'GET',
35+
});
36+
37+
req.flush({});
38+
httpTestingController.verify();
39+
expect().nothing();
40+
done();
41+
});
42+
43+
describe(`when the GET succeeds`, () => {
44+
it(`should set tokenInitialized$ to true`, (done: DoneFn) => {
45+
service.initXSRFToken(httpClient)();
46+
47+
const req = httpTestingController.expectOne(endpointURL);
48+
49+
req.flush({});
50+
httpTestingController.verify();
51+
52+
expect(service.tokenInitialized$.getValue()).toBeTrue();
53+
done();
54+
});
55+
});
56+
57+
});
58+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { Injectable } from '@angular/core';
3+
import { take } from 'rxjs/operators';
4+
5+
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
6+
import { XSRFService } from './xsrf.service';
7+
8+
/**
9+
* Browser (CSR) Service to obtain a new CSRF/XSRF token when needed by our RequestService
10+
* to perform a modify request (e.g. POST/PUT/DELETE).
11+
* NOTE: This is primarily necessary before the *first* modifying request, as the CSRF
12+
* token may not yet be initialized.
13+
*/
14+
@Injectable()
15+
export class BrowserXSRFService extends XSRFService {
16+
initXSRFToken(httpClient: HttpClient): () => Promise<any> {
17+
return () => new Promise<void>((resolve) => {
18+
// Force a new token to be created by calling the CSRF endpoint
19+
httpClient.get(new RESTURLCombiner('/security/csrf').toString(), undefined).pipe(
20+
take(1),
21+
).subscribe(() => {
22+
// Once token is returned, set tokenInitialized to true.
23+
this.tokenInitialized$.next(true);
24+
});
25+
26+
// return immediately, the rest of the app doesn't need to wait for this to finish
27+
resolve();
28+
});
29+
}
30+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { HttpClient } from '@angular/common/http';
2+
3+
import { ServerXSRFService } from './server-xsrf.service';
4+
5+
describe(`ServerXSRFService`, () => {
6+
let service: ServerXSRFService;
7+
let httpClient: HttpClient;
8+
9+
beforeEach(() => {
10+
httpClient = jasmine.createSpyObj(['post', 'get', 'request']);
11+
service = new ServerXSRFService();
12+
});
13+
14+
describe(`initXSRFToken`, () => {
15+
it(`shouldn't perform any requests`, (done: DoneFn) => {
16+
service.initXSRFToken(httpClient)().then(() => {
17+
for (const prop in httpClient) {
18+
if (httpClient.hasOwnProperty(prop)) {
19+
expect(httpClient[prop]).not.toHaveBeenCalled();
20+
}
21+
}
22+
done();
23+
});
24+
});
25+
26+
it(`should leave tokenInitialized$ on false`, (done: DoneFn) => {
27+
service.initXSRFToken(httpClient)().then(() => {
28+
expect(service.tokenInitialized$.getValue()).toBeFalse();
29+
done();
30+
});
31+
});
32+
});
33+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { Injectable } from '@angular/core';
3+
4+
import { XSRFService } from './xsrf.service';
5+
6+
/**
7+
* Server (SSR) Service to obtain a new CSRF/XSRF token. Because SSR only triggers GET
8+
* requests a CSRF token is never needed.
9+
*/
10+
@Injectable()
11+
export class ServerXSRFService extends XSRFService {
12+
initXSRFToken(httpClient: HttpClient): () => Promise<any> {
13+
return () => new Promise<void>((resolve) => {
14+
// return immediately, and keep tokenInitialized$ false. The server side can make only GET
15+
// requests, since it can never get a valid XSRF cookie
16+
resolve();
17+
});
18+
}
19+
}

0 commit comments

Comments
 (0)