mirror of
https://github.com/google/nomulus.git
synced 2025-07-23 19:20:44 +02:00
Add Console Settings -> Security front-end (#2079)
This commit is contained in:
parent
9873772150
commit
9b17adcb28
19 changed files with 545 additions and 21 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -31,6 +31,7 @@ tmp/
|
||||||
local.properties
|
local.properties
|
||||||
.settings/
|
.settings/
|
||||||
.loadpath
|
.loadpath
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Eclipse Core
|
# Eclipse Core
|
||||||
.project
|
.project
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
"/console-api":
|
"/console-api":
|
||||||
{
|
{
|
||||||
"target": "http://localhost:8080",
|
"target": "http://localhost:8080",
|
||||||
"secure": true
|
"secure": false,
|
||||||
|
"logLevel": "debug",
|
||||||
|
"changeOrigin": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve --proxy-config dev-proxy.config.json",
|
||||||
"build": "ng build --base-href=/console/",
|
"build": "ng build --base-href=/console/",
|
||||||
"build:local": "ng build --base-href=/default/console/",
|
"build:local": "ng build --base-href=/default/console/",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="console-app">
|
<div class="console-app">
|
||||||
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
|
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
|
||||||
<mat-sidenav-container class="console-app__content-wrapper">
|
<mat-sidenav-container class="console-app__container">
|
||||||
<mat-sidenav #sidenav class="console-app__sidebar">
|
<mat-sidenav #sidenav class="console-app__sidebar">
|
||||||
<mat-nav-list>
|
<mat-nav-list>
|
||||||
<a mat-list-item [routerLink]="'/home'" routerLinkActive="active">
|
<a mat-list-item [routerLink]="'/home'" routerLinkActive="active">
|
||||||
|
@ -17,8 +17,10 @@
|
||||||
</a>
|
</a>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content class="console-app__content">
|
<mat-sidenav-content class="console-app__content-wrapper">
|
||||||
<router-outlet></router-outlet>
|
<div class="console-app__content">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
</mat-sidenav-container>
|
</mat-sidenav-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
&__content-wrapper {
|
&__container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-top: -12px;
|
margin-top: -12px;
|
||||||
padding-bottom: 36px;
|
padding-bottom: 36px;
|
||||||
|
@ -40,7 +40,11 @@
|
||||||
background: #eae1e1;
|
background: #eae1e1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&__content {
|
&__content-wrapper {
|
||||||
margin: 12px 24px;
|
margin: 12px 24px;
|
||||||
}
|
}
|
||||||
|
&__content {
|
||||||
|
max-width: 1340px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,13 @@
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MaterialModule } from './material.module';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RouterTestingModule],
|
imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule],
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,6 +33,7 @@ import SettingsContactComponent, {
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { RegistrarComponent } from './registrar/registrar.component';
|
import { RegistrarComponent } from './registrar/registrar.component';
|
||||||
import { RegistrarGuard } from './registrar/registrar.guard';
|
import { RegistrarGuard } from './registrar/registrar.guard';
|
||||||
|
import SecurityComponent from './settings/security/security.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -44,6 +45,7 @@ import { RegistrarGuard } from './registrar/registrar.guard';
|
||||||
SettingsContactComponent,
|
SettingsContactComponent,
|
||||||
ContactDetailsDialogComponent,
|
ContactDetailsDialogComponent,
|
||||||
RegistrarComponent,
|
RegistrarComponent,
|
||||||
|
SecurityComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MaterialModule } from '../material.module';
|
||||||
|
|
||||||
describe('HeaderComponent', () => {
|
describe('HeaderComponent', () => {
|
||||||
let component: HeaderComponent;
|
let component: HeaderComponent;
|
||||||
|
@ -22,6 +24,7 @@ describe('HeaderComponent', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MaterialModule, BrowserAnimationsModule],
|
||||||
declarations: [HeaderComponent],
|
declarations: [HeaderComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { RegistrarComponent } from './registrar.component';
|
||||||
import { BackendService } from '../shared/services/backend.service';
|
import { BackendService } from '../shared/services/backend.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MaterialModule } from '../material.module';
|
||||||
|
|
||||||
describe('RegistrarComponent', () => {
|
describe('RegistrarComponent', () => {
|
||||||
let component: RegistrarComponent;
|
let component: RegistrarComponent;
|
||||||
|
@ -26,7 +28,11 @@ describe('RegistrarComponent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [RegistrarComponent],
|
declarations: [RegistrarComponent],
|
||||||
imports: [HttpClientTestingModule],
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
MaterialModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
BackendService,
|
BackendService,
|
||||||
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
|
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
>
|
>
|
||||||
</section>
|
</section>
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button (click)="onClose()">Cancel</button>
|
<button mat-button (click)="onClose($event)">Cancel</button>
|
||||||
<button type="submit" mat-button>Save</button>
|
<button type="submit" mat-button>Save</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -80,11 +80,11 @@ class ContactDetailsEventsResponder {
|
||||||
styleUrls: ['./contact.component.less'],
|
styleUrls: ['./contact.component.less'],
|
||||||
})
|
})
|
||||||
export class ContactDetailsDialogComponent {
|
export class ContactDetailsDialogComponent {
|
||||||
onClose!: Function;
|
|
||||||
contact: Contact;
|
contact: Contact;
|
||||||
contactTypes = contactTypes;
|
contactTypes = contactTypes;
|
||||||
operation: Operations;
|
operation: Operations;
|
||||||
contactIndex: number;
|
contactIndex: number;
|
||||||
|
onCloseCallback: Function;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public contactService: ContactService,
|
public contactService: ContactService,
|
||||||
|
@ -96,7 +96,7 @@ export class ContactDetailsDialogComponent {
|
||||||
operation: Operations;
|
operation: Operations;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
this.onClose = data.onClose;
|
this.onCloseCallback = data.onClose;
|
||||||
this.contactIndex = contactService.contacts.findIndex(
|
this.contactIndex = contactService.contacts.findIndex(
|
||||||
(c) => c === data.contact
|
(c) => c === data.contact
|
||||||
);
|
);
|
||||||
|
@ -104,9 +104,14 @@ export class ContactDetailsDialogComponent {
|
||||||
this.operation = data.operation;
|
this.operation = data.operation;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveAndClose(e: any) {
|
onClose(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!e.target.checkValidity()) {
|
this.onCloseCallback.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAndClose(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!(e.target as HTMLFormElement).checkValidity()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let operationObservable;
|
let operationObservable;
|
||||||
|
@ -122,7 +127,7 @@ export class ContactDetailsDialogComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
operationObservable.subscribe({
|
operationObservable.subscribe({
|
||||||
complete: this.onClose.bind(this),
|
complete: this.onCloseCallback.bind(this),
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
this._snackBar.open(err.statusText, undefined, {
|
this._snackBar.open(err.statusText, undefined, {
|
||||||
duration: 1500,
|
duration: 1500,
|
||||||
|
@ -169,7 +174,7 @@ export default class ContactComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreateNew(e: Event) {
|
openCreateNew(e: MouseEvent) {
|
||||||
const newContact: Contact = {
|
const newContact: Contact = {
|
||||||
name: '',
|
name: '',
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
|
@ -180,7 +185,7 @@ export default class ContactComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
openDetails(
|
openDetails(
|
||||||
e: Event,
|
e: MouseEvent,
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
operation: Operations = Operations.UPDATE
|
operation: Operations = Operations.UPDATE
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1 +1,96 @@
|
||||||
<p>security works!</p>
|
<div *ngIf="loading" class="settings-security__loading">
|
||||||
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
|
</div>
|
||||||
|
<div class="settings-security" *ngIf="!loading">
|
||||||
|
<div class="settings-security__section">
|
||||||
|
<div class="settings-security__section-description">
|
||||||
|
<h2>IP Allowlist</h2>
|
||||||
|
<p>
|
||||||
|
Restrict access to EPP production servers to the following IP/IPv6
|
||||||
|
addresses, or ranges like 1.1.1.0/24
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-security__section-form">
|
||||||
|
<div
|
||||||
|
*ngIf="
|
||||||
|
dataSource.ipAddressAllowList &&
|
||||||
|
dataSource.ipAddressAllowList.length > 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div *ngFor="let item of dataSource.ipAddressAllowList; index as index">
|
||||||
|
<div>{{ item.value }}</div>
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
class="settings-security__ip-allowlist"
|
||||||
|
[(ngModel)]="item.value"
|
||||||
|
[disabled]="!inEdit"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
*ngIf="inEdit"
|
||||||
|
matSuffix
|
||||||
|
mat-icon-button
|
||||||
|
aria-label="Remove"
|
||||||
|
(click)="removeIpEntry(index)"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button mat-stroked-button (click)="enableEdit(); createIpEntry()">
|
||||||
|
Add IP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-security__section">
|
||||||
|
<div class="settings-security__section-description">
|
||||||
|
<h2>SSL Certificate</h2>
|
||||||
|
<p>X.509 PEM certificate for EPP production access.</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-security__section-form">
|
||||||
|
<textarea
|
||||||
|
matInput
|
||||||
|
class="settings-security__clientCertificate"
|
||||||
|
[(ngModel)]="dataSource.clientCertificate"
|
||||||
|
[disabled]="!inEdit"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-security__section">
|
||||||
|
<div class="settings-security__section-description">
|
||||||
|
<h2>Failover SSL Certificate</h2>
|
||||||
|
<p>X.509 PEM backup certificate for EPP Production Access.</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-security__section-form">
|
||||||
|
<textarea
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="dataSource.failoverClientCertificate"
|
||||||
|
[disabled]="!inEdit"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-security__actions">
|
||||||
|
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
|
||||||
|
<button
|
||||||
|
class="settings-security__actions-save"
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
(click)="save()"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="settings-security__actions-cancel"
|
||||||
|
mat-stroked-button
|
||||||
|
(click)="cancel()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #inView>
|
||||||
|
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -11,3 +11,40 @@
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,18 +12,56 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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 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', () => {
|
describe('SecurityComponent', () => {
|
||||||
let component: SecurityComponent;
|
let component: SecurityComponent;
|
||||||
let fixture: ComponentFixture<SecurityComponent>;
|
let fixture: ComponentFixture<SecurityComponent>;
|
||||||
|
let fetchSecurityDetailsSpy: Function;
|
||||||
|
let saveSpy: Function;
|
||||||
|
|
||||||
beforeEach(async () => {
|
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({
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
MaterialModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
FormsModule,
|
||||||
|
],
|
||||||
declarations: [SecurityComponent],
|
declarations: [SecurityComponent],
|
||||||
}).compileComponents();
|
providers: [BackendService],
|
||||||
|
})
|
||||||
|
.overrideComponent(SecurityComponent, {
|
||||||
|
set: {
|
||||||
|
providers: [
|
||||||
|
{ provide: SecurityService, useValue: securityServiceSpy },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(SecurityComponent);
|
fixture = TestBed.createComponent(SecurityComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
@ -33,4 +71,86 @@ describe('SecurityComponent', () => {
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
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',
|
||||||
|
});
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,10 +13,79 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
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({
|
@Component({
|
||||||
selector: 'app-security',
|
selector: 'app-security',
|
||||||
templateUrl: './security.component.html',
|
templateUrl: './security.component.html',
|
||||||
styleUrls: ['./security.component.less'],
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
86
console-webapp/src/app/settings/security/security.service.ts
Normal file
86
console-webapp/src/app/settings/security/security.service.ts
Normal file
|
@ -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<ipAllowListItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecuritySettingsBackendModel {
|
||||||
|
clientCertificate?: string;
|
||||||
|
failoverClientCertificate?: string;
|
||||||
|
ipAddressAllowList?: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,8 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { SettingsComponent } from './settings.component';
|
import { SettingsComponent } from './settings.component';
|
||||||
|
import { MaterialModule } from '../material.module';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
describe('SettingsComponent', () => {
|
describe('SettingsComponent', () => {
|
||||||
let component: SettingsComponent;
|
let component: SettingsComponent;
|
||||||
|
@ -22,6 +24,7 @@ describe('SettingsComponent', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MaterialModule, BrowserAnimationsModule],
|
||||||
declarations: [SettingsComponent],
|
declarations: [SettingsComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Observable, catchError, of } from 'rxjs';
|
import { Observable, catchError, of } from 'rxjs';
|
||||||
import { Contact } from '../../settings/contact/contact.service';
|
import { Contact } from '../../settings/contact/contact.service';
|
||||||
|
import { SecuritySettingsBackendModel } from 'src/app/settings/security/security.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BackendService {
|
export class BackendService {
|
||||||
|
@ -63,4 +64,28 @@ export class BackendService {
|
||||||
.get<string[]>('/console-api/registrars')
|
.get<string[]>('/console-api/registrars')
|
||||||
.pipe(catchError((err) => this.errorCatcher<string[]>(err)));
|
.pipe(catchError((err) => this.errorCatcher<string[]>(err)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSecuritySettings(
|
||||||
|
registrarId: string
|
||||||
|
): Observable<SecuritySettingsBackendModel> {
|
||||||
|
return this.http
|
||||||
|
.get<SecuritySettingsBackendModel>(
|
||||||
|
`/console-api/settings/security?registrarId=${registrarId}`
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
catchError((err) =>
|
||||||
|
this.errorCatcher<SecuritySettingsBackendModel>(err)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postSecuritySettings(
|
||||||
|
registrarId: string,
|
||||||
|
securitySettings: SecuritySettingsBackendModel
|
||||||
|
): Observable<SecuritySettingsBackendModel> {
|
||||||
|
return this.http.post<SecuritySettingsBackendModel>(
|
||||||
|
`/console-api/settings/security?registrarId=${registrarId}`,
|
||||||
|
{ registrar: securitySettings }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue