From 9b17adcb28b6f8aac76ca55fcf04a3a53d9f9933 Mon Sep 17 00:00:00 2001 From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:50:31 -0400 Subject: [PATCH] Add Console Settings -> Security front-end (#2079) --- .gitignore | 1 + console-webapp/dev-proxy.config.json | 4 +- console-webapp/package.json | 2 +- console-webapp/src/app/app.component.html | 8 +- console-webapp/src/app/app.component.less | 8 +- console-webapp/src/app/app.component.spec.ts | 4 +- console-webapp/src/app/app.module.ts | 2 + .../src/app/header/header.component.spec.ts | 3 + .../app/registrar/registrar.component.spec.ts | 8 +- .../contact/contact-details.component.html | 2 +- .../app/settings/contact/contact.component.ts | 19 ++- .../settings/security/security.component.html | 97 +++++++++++++- .../settings/security/security.component.less | 37 ++++++ .../security/security.component.spec.ts | 124 +++++++++++++++++- .../settings/security/security.component.ts | 71 +++++++++- .../security/security.service.spec.ts | 62 +++++++++ .../app/settings/security/security.service.ts | 86 ++++++++++++ .../app/settings/settings.component.spec.ts | 3 + .../app/shared/services/backend.service.ts | 25 ++++ 19 files changed, 545 insertions(+), 21 deletions(-) create mode 100644 console-webapp/src/app/settings/security/security.service.spec.ts create mode 100644 console-webapp/src/app/settings/security/security.service.ts diff --git a/.gitignore b/.gitignore index 6cbd10b37..e88c60894 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ tmp/ local.properties .settings/ .loadpath +.DS_Store # Eclipse Core .project diff --git a/console-webapp/dev-proxy.config.json b/console-webapp/dev-proxy.config.json index 7c6e2782d..aee00a9a3 100644 --- a/console-webapp/dev-proxy.config.json +++ b/console-webapp/dev-proxy.config.json @@ -2,6 +2,8 @@ "/console-api": { "target": "http://localhost:8080", - "secure": true + "secure": false, + "logLevel": "debug", + "changeOrigin": true } } diff --git a/console-webapp/package.json b/console-webapp/package.json index 5b4d1d57e..584134388 100644 --- a/console-webapp/package.json +++ b/console-webapp/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", + "start": "ng serve --proxy-config dev-proxy.config.json", "build": "ng build --base-href=/console/", "build:local": "ng build --base-href=/default/console/", "watch": "ng build --watch --configuration development", diff --git a/console-webapp/src/app/app.component.html b/console-webapp/src/app/app.component.html index 078060a48..1c6f6cfba 100644 --- a/console-webapp/src/app/app.component.html +++ b/console-webapp/src/app/app.component.html @@ -1,6 +1,6 @@
- + @@ -17,8 +17,10 @@ - - + +
+ +
diff --git a/console-webapp/src/app/app.component.less b/console-webapp/src/app/app.component.less index 6da45accc..baeb4a306 100644 --- a/console-webapp/src/app/app.component.less +++ b/console-webapp/src/app/app.component.less @@ -26,7 +26,7 @@ display: flex; flex-direction: column; height: 100%; - &__content-wrapper { + &__container { flex: 1; margin-top: -12px; padding-bottom: 36px; @@ -40,7 +40,11 @@ background: #eae1e1; } } - &__content { + &__content-wrapper { margin: 12px 24px; } + &__content { + max-width: 1340px; + margin: 0 auto; + } } diff --git a/console-webapp/src/app/app.component.spec.ts b/console-webapp/src/app/app.component.spec.ts index 4569897db..0f20a24a8 100644 --- a/console-webapp/src/app/app.component.spec.ts +++ b/console-webapp/src/app/app.component.spec.ts @@ -15,11 +15,13 @@ import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MaterialModule } from './material.module'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RouterTestingModule], + imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule], declarations: [AppComponent], }).compileComponents(); }); diff --git a/console-webapp/src/app/app.module.ts b/console-webapp/src/app/app.module.ts index f18508009..604637b85 100644 --- a/console-webapp/src/app/app.module.ts +++ b/console-webapp/src/app/app.module.ts @@ -33,6 +33,7 @@ import SettingsContactComponent, { import { HttpClientModule } from '@angular/common/http'; import { RegistrarComponent } from './registrar/registrar.component'; import { RegistrarGuard } from './registrar/registrar.guard'; +import SecurityComponent from './settings/security/security.component'; @NgModule({ declarations: [ @@ -44,6 +45,7 @@ import { RegistrarGuard } from './registrar/registrar.guard'; SettingsContactComponent, ContactDetailsDialogComponent, RegistrarComponent, + SecurityComponent, ], imports: [ HttpClientModule, diff --git a/console-webapp/src/app/header/header.component.spec.ts b/console-webapp/src/app/header/header.component.spec.ts index 36351ab0a..579b7166e 100644 --- a/console-webapp/src/app/header/header.component.spec.ts +++ b/console-webapp/src/app/header/header.component.spec.ts @@ -15,6 +15,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HeaderComponent } from './header.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MaterialModule } from '../material.module'; describe('HeaderComponent', () => { let component: HeaderComponent; @@ -22,6 +24,7 @@ describe('HeaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [MaterialModule, BrowserAnimationsModule], declarations: [HeaderComponent], }).compileComponents(); diff --git a/console-webapp/src/app/registrar/registrar.component.spec.ts b/console-webapp/src/app/registrar/registrar.component.spec.ts index 861a0f1b3..1166a58a1 100644 --- a/console-webapp/src/app/registrar/registrar.component.spec.ts +++ b/console-webapp/src/app/registrar/registrar.component.spec.ts @@ -18,6 +18,8 @@ import { RegistrarComponent } from './registrar.component'; import { BackendService } from '../shared/services/backend.service'; import { ActivatedRoute } from '@angular/router'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MaterialModule } from '../material.module'; describe('RegistrarComponent', () => { let component: RegistrarComponent; @@ -26,7 +28,11 @@ describe('RegistrarComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [RegistrarComponent], - imports: [HttpClientTestingModule], + imports: [ + HttpClientTestingModule, + MaterialModule, + BrowserAnimationsModule, + ], providers: [ BackendService, { provide: ActivatedRoute, useValue: {} as ActivatedRoute }, diff --git a/console-webapp/src/app/settings/contact/contact-details.component.html b/console-webapp/src/app/settings/contact/contact-details.component.html index 73ffcd40a..c83438f27 100644 --- a/console-webapp/src/app/settings/contact/contact-details.component.html +++ b/console-webapp/src/app/settings/contact/contact-details.component.html @@ -97,7 +97,7 @@ > - + diff --git a/console-webapp/src/app/settings/contact/contact.component.ts b/console-webapp/src/app/settings/contact/contact.component.ts index 4f025b514..1337f4ca6 100644 --- a/console-webapp/src/app/settings/contact/contact.component.ts +++ b/console-webapp/src/app/settings/contact/contact.component.ts @@ -80,11 +80,11 @@ class ContactDetailsEventsResponder { styleUrls: ['./contact.component.less'], }) export class ContactDetailsDialogComponent { - onClose!: Function; contact: Contact; contactTypes = contactTypes; operation: Operations; contactIndex: number; + onCloseCallback: Function; constructor( public contactService: ContactService, @@ -96,7 +96,7 @@ export class ContactDetailsDialogComponent { operation: Operations; } ) { - this.onClose = data.onClose; + this.onCloseCallback = data.onClose; this.contactIndex = contactService.contacts.findIndex( (c) => c === data.contact ); @@ -104,9 +104,14 @@ export class ContactDetailsDialogComponent { this.operation = data.operation; } - saveAndClose(e: any) { + onClose(e: MouseEvent) { e.preventDefault(); - if (!e.target.checkValidity()) { + this.onCloseCallback.call(this); + } + + saveAndClose(e: SubmitEvent) { + e.preventDefault(); + if (!(e.target as HTMLFormElement).checkValidity()) { return; } let operationObservable; @@ -122,7 +127,7 @@ export class ContactDetailsDialogComponent { } operationObservable.subscribe({ - complete: this.onClose.bind(this), + complete: this.onCloseCallback.bind(this), error: (err: HttpErrorResponse) => { this._snackBar.open(err.statusText, undefined, { duration: 1500, @@ -169,7 +174,7 @@ export default class ContactComponent { } } - openCreateNew(e: Event) { + openCreateNew(e: MouseEvent) { const newContact: Contact = { name: '', phoneNumber: '', @@ -180,7 +185,7 @@ export default class ContactComponent { } openDetails( - e: Event, + e: MouseEvent, contact: Contact, operation: Operations = Operations.UPDATE ) { diff --git a/console-webapp/src/app/settings/security/security.component.html b/console-webapp/src/app/settings/security/security.component.html index ac03c4d0b..62639465b 100644 --- a/console-webapp/src/app/settings/security/security.component.html +++ b/console-webapp/src/app/settings/security/security.component.html @@ -1 +1,96 @@ -

security works!

+
+ +
+
+
+
+

IP Allowlist

+

+ Restrict access to EPP production servers to the following IP/IPv6 + addresses, or ranges like 1.1.1.0/24 +

+
+
+
+
+
{{ item.value }}
+ + + + +
+
+ +
+
+
+
+

SSL Certificate

+

X.509 PEM certificate for EPP production access.

+
+
+ +
+
+
+
+

Failover SSL Certificate

+

X.509 PEM backup certificate for EPP Production Access.

+
+
+ +
+
+
+ + + + + + + +
+
diff --git a/console-webapp/src/app/settings/security/security.component.less b/console-webapp/src/app/settings/security/security.component.less index 5b5b64371..afc6c375f 100644 --- a/console-webapp/src/app/settings/security/security.component.less +++ b/console-webapp/src/app/settings/security/security.component.less @@ -11,3 +11,40 @@ // 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. + +.settings-security { + margin-top: 1.5rem; + h1 { + margin: 0; + } + &__section { + display: flex; + align-items: stretch; + margin-bottom: 3rem; + flex-wrap: wrap; + } + &__section-description { + flex: 1; + min-width: 300px; + } + &__section-form { + flex: 1; + min-width: 300px; + textarea { + min-height: 100%; + min-width: 100%; + box-sizing: border-box; + min-height: 100px; + } + } + &__actions { + display: flex; + justify-content: flex-end; + button { + margin-left: 20px; + } + } + &__loading { + margin: 2rem 0; + } +} diff --git a/console-webapp/src/app/settings/security/security.component.spec.ts b/console-webapp/src/app/settings/security/security.component.spec.ts index 1f88a9551..c7c3885a8 100644 --- a/console-webapp/src/app/settings/security/security.component.spec.ts +++ b/console-webapp/src/app/settings/security/security.component.spec.ts @@ -12,18 +12,56 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import SecurityComponent from './security.component'; +import { SecurityService } from './security.service'; +import { BackendService } from 'src/app/shared/services/backend.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MaterialModule } from 'src/app/material.module'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { of } from 'rxjs'; +import { FormsModule } from '@angular/forms'; describe('SecurityComponent', () => { let component: SecurityComponent; let fixture: ComponentFixture; + let fetchSecurityDetailsSpy: Function; + let saveSpy: Function; beforeEach(async () => { + const securityServiceSpy = jasmine.createSpyObj(SecurityService, [ + 'fetchSecurityDetails', + 'saveChanges', + ]); + + fetchSecurityDetailsSpy = + securityServiceSpy.fetchSecurityDetails.and.returnValue(of()); + + saveSpy = securityServiceSpy.saveChanges; + + securityServiceSpy.securitySettings = { + ipAddressAllowList: [{ value: '123.123.123.123' }], + }; + await TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + MaterialModule, + BrowserAnimationsModule, + FormsModule, + ], declarations: [SecurityComponent], - }).compileComponents(); + providers: [BackendService], + }) + .overrideComponent(SecurityComponent, { + set: { + providers: [ + { provide: SecurityService, useValue: securityServiceSpy }, + ], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(SecurityComponent); component = fixture.componentInstance; @@ -33,4 +71,86 @@ describe('SecurityComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call fetch spy', () => { + expect(fetchSecurityDetailsSpy).toHaveBeenCalledTimes(1); + }); + + it('should render ip allow list', waitForAsync(() => { + component.enableEdit(); + fixture.whenStable().then(() => { + expect( + Array.from( + fixture.nativeElement.querySelectorAll( + '.settings-security__ip-allowlist' + ) + ) + ).toHaveSize(1); + expect( + fixture.nativeElement.querySelector('.settings-security__ip-allowlist') + .value + ).toBe('123.123.123.123'); + }); + })); + + it('should remove ip', waitForAsync(() => { + expect( + Array.from( + fixture.nativeElement.querySelectorAll( + '.settings-security__ip-allowlist' + ) + ) + ).toHaveSize(1); + component.removeIpEntry(0); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect( + Array.from( + fixture.nativeElement.querySelectorAll( + '.settings-security__ip-allowlist' + ) + ) + ).toHaveSize(0); + }); + })); + + it('should toggle inEdit', () => { + expect(component.inEdit).toBeFalse(); + component.enableEdit(); + expect(component.inEdit).toBeTrue(); + }); + + it('should create temporary data structure', () => { + expect(component.dataSource).toBe( + component.securityService.securitySettings + ); + component.enableEdit(); + expect(component.dataSource).not.toBe( + component.securityService.securitySettings + ); + component.cancel(); + expect(component.dataSource).toBe( + component.securityService.securitySettings + ); + }); + + it('should call save', waitForAsync(async () => { + component.enableEdit(); + fixture.detectChanges(); + await fixture.whenStable(); + const el = fixture.nativeElement.querySelector( + '.settings-security__clientCertificate' + ); + el.value = 'test'; + el.dispatchEvent(new Event('input')); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.nativeElement + .querySelector('.settings-security__actions-save') + .click(); + expect(saveSpy).toHaveBeenCalledOnceWith({ + ipAddressAllowList: [{ value: '123.123.123.123' }], + clientCertificate: 'test', + }); + })); }); diff --git a/console-webapp/src/app/settings/security/security.component.ts b/console-webapp/src/app/settings/security/security.component.ts index 8f6d85c3d..9eb602bda 100644 --- a/console-webapp/src/app/settings/security/security.component.ts +++ b/console-webapp/src/app/settings/security/security.component.ts @@ -13,10 +13,79 @@ // limitations under the License. import { Component } from '@angular/core'; +import { SecurityService, SecuritySettings } from './security.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-security', templateUrl: './security.component.html', styleUrls: ['./security.component.less'], + providers: [SecurityService], }) -export default class SecurityComponent {} +export default class SecurityComponent { + loading: boolean = false; + inEdit: boolean = false; + dataSource: SecuritySettings = {}; + + constructor( + public securityService: SecurityService, + private _snackBar: MatSnackBar + ) { + this.loading = true; + this.securityService.fetchSecurityDetails().subscribe({ + complete: () => { + this.dataSource = this.securityService.securitySettings; + this.loading = false; + }, + error: (err: HttpErrorResponse) => { + this._snackBar.open(err.error, undefined, { + duration: 1500, + }); + this.loading = false; + }, + }); + } + + enableEdit() { + this.inEdit = true; + this.dataSource = JSON.parse( + JSON.stringify(this.securityService.securitySettings) + ); + } + + disableEdit() { + this.inEdit = false; + this.dataSource = this.securityService.securitySettings; + } + + createIpEntry() { + this.dataSource.ipAddressAllowList?.push({ value: '' }); + } + + save() { + this.loading = true; + this.securityService.saveChanges(this.dataSource).subscribe({ + complete: () => { + this.loading = false; + this.dataSource = this.securityService.securitySettings; + }, + error: (err: HttpErrorResponse) => { + this._snackBar.open(err.error, undefined, { + duration: 1500, + }); + }, + }); + this.disableEdit(); + } + + cancel() { + this.dataSource = this.securityService.securitySettings; + this.inEdit = false; + } + + removeIpEntry(index: number) { + this.dataSource.ipAddressAllowList = + this.dataSource.ipAddressAllowList?.filter((_, i) => i != index); + } +} diff --git a/console-webapp/src/app/settings/security/security.service.spec.ts b/console-webapp/src/app/settings/security/security.service.spec.ts new file mode 100644 index 000000000..cd42e3b60 --- /dev/null +++ b/console-webapp/src/app/settings/security/security.service.spec.ts @@ -0,0 +1,62 @@ +// Copyright 2023 The Nomulus Authors. 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 { TestBed } from '@angular/core/testing'; + +import { + SecurityService, + SecuritySettings, + SecuritySettingsBackendModel, + apiToUiConverter, + uiToApiConverter, +} from './security.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import SecurityComponent from './security.component'; +import { BackendService } from 'src/app/shared/services/backend.service'; + +describe('SecurityService', () => { + const uiMockData: SecuritySettings = { + clientCertificate: 'clientCertificateTest', + failoverClientCertificate: 'failoverClientCertificateTest', + ipAddressAllowList: [{ value: '123.123.123.123' }], + }; + const apiMockData: SecuritySettingsBackendModel = { + clientCertificate: 'clientCertificateTest', + failoverClientCertificate: 'failoverClientCertificateTest', + ipAddressAllowList: ['123.123.123.123'], + }; + + let service: SecurityService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [SecurityComponent], + providers: [SecurityService, BackendService], + }); + service = TestBed.inject(SecurityService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should convert from api to ui', () => { + expect(apiToUiConverter(apiMockData)).toEqual(uiMockData); + }); + + it('should convert from ui to api', () => { + expect(uiToApiConverter(uiMockData)).toEqual(apiMockData); + }); +}); diff --git a/console-webapp/src/app/settings/security/security.service.ts b/console-webapp/src/app/settings/security/security.service.ts new file mode 100644 index 000000000..ae19deb8e --- /dev/null +++ b/console-webapp/src/app/settings/security/security.service.ts @@ -0,0 +1,86 @@ +// Copyright 2023 The Nomulus Authors. 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 { tap } from 'rxjs'; +import { RegistrarService } from 'src/app/registrar/registrar.service'; +import { BackendService } from 'src/app/shared/services/backend.service'; + +interface ipAllowListItem { + value: string; +} +export interface SecuritySettings { + clientCertificate?: string; + failoverClientCertificate?: string; + ipAddressAllowList?: Array; +} + +export interface SecuritySettingsBackendModel { + clientCertificate?: string; + failoverClientCertificate?: string; + ipAddressAllowList?: Array; +} + +export function apiToUiConverter( + securitySettings: SecuritySettingsBackendModel = {} +): SecuritySettings { + return Object.assign({}, securitySettings, { + ipAddressAllowList: (securitySettings.ipAddressAllowList || []).map( + (value) => ({ value }) + ), + }); +} + +export function uiToApiConverter( + securitySettings: SecuritySettings +): SecuritySettingsBackendModel { + return Object.assign({}, securitySettings, { + ipAddressAllowList: (securitySettings.ipAddressAllowList || []) + .filter((s) => s.value) + .map((ipAllowItem: ipAllowListItem) => ipAllowItem.value), + }); +} + +@Injectable() +export class SecurityService { + securitySettings: SecuritySettings = {}; + + constructor( + private backend: BackendService, + private registrarService: RegistrarService + ) {} + + fetchSecurityDetails() { + return this.backend + .getSecuritySettings(this.registrarService.activeRegistrarId) + .pipe( + tap((securitySettings: SecuritySettingsBackendModel) => { + this.securitySettings = apiToUiConverter(securitySettings); + }) + ); + } + + saveChanges(newSecuritySettings: SecuritySettings) { + return this.backend + .postSecuritySettings( + this.registrarService.activeRegistrarId, + uiToApiConverter(newSecuritySettings) + ) + .pipe( + tap((_) => { + this.securitySettings = newSecuritySettings; + }) + ); + } +} diff --git a/console-webapp/src/app/settings/settings.component.spec.ts b/console-webapp/src/app/settings/settings.component.spec.ts index 5fe97f254..128d15192 100644 --- a/console-webapp/src/app/settings/settings.component.spec.ts +++ b/console-webapp/src/app/settings/settings.component.spec.ts @@ -15,6 +15,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SettingsComponent } from './settings.component'; +import { MaterialModule } from '../material.module'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; describe('SettingsComponent', () => { let component: SettingsComponent; @@ -22,6 +24,7 @@ describe('SettingsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [MaterialModule, BrowserAnimationsModule], declarations: [SettingsComponent], }).compileComponents(); diff --git a/console-webapp/src/app/shared/services/backend.service.ts b/console-webapp/src/app/shared/services/backend.service.ts index 5aab46eeb..440653d0d 100644 --- a/console-webapp/src/app/shared/services/backend.service.ts +++ b/console-webapp/src/app/shared/services/backend.service.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, catchError, of } from 'rxjs'; import { Contact } from '../../settings/contact/contact.service'; +import { SecuritySettingsBackendModel } from 'src/app/settings/security/security.service'; @Injectable() export class BackendService { @@ -63,4 +64,28 @@ export class BackendService { .get('/console-api/registrars') .pipe(catchError((err) => this.errorCatcher(err))); } + + getSecuritySettings( + registrarId: string + ): Observable { + return this.http + .get( + `/console-api/settings/security?registrarId=${registrarId}` + ) + .pipe( + catchError((err) => + this.errorCatcher(err) + ) + ); + } + + postSecuritySettings( + registrarId: string, + securitySettings: SecuritySettingsBackendModel + ): Observable { + return this.http.post( + `/console-api/settings/security?registrarId=${registrarId}`, + { registrar: securitySettings } + ); + } }