Implement scan all policy configuration

This commit is contained in:
Steven Zou 2017-06-29 18:41:30 +08:00
parent 22a4e91a79
commit 1db36d99fb
29 changed files with 829 additions and 93 deletions

View File

@ -41,7 +41,18 @@ export class BoolValueItem {
}
}
export class ComplexValueItem {
value: any | { [key: string]: any | any[] };
editable: boolean;
public constructor(v: any | { [key: string]: any | any[] }, e: boolean) {
this.value = v;
this.editable = e;
}
}
export class Configuration {
[key: string]: any | any[]
auth_mode: StringValueItem;
project_creation_restriction: StringValueItem;
self_registration: BoolValueItem;
@ -63,6 +74,7 @@ export class Configuration {
verify_remote_cert: BoolValueItem;
token_expiration: NumberValueItem;
cfg_expiration: NumberValueItem;
scan_all_policy: ComplexValueItem;
public constructor() {
this.auth_mode = new StringValueItem("db_auth", true);
@ -83,8 +95,14 @@ export class Configuration {
this.email_ssl = new BoolValueItem(false, true);
this.email_username = new StringValueItem("", true);
this.email_password = new StringValueItem("", true);
this.token_expiration = new NumberValueItem(5, true);
this.token_expiration = new NumberValueItem(30, true);
this.cfg_expiration = new NumberValueItem(30, true);
this.verify_remote_cert = new BoolValueItem(false, true);
this.scan_all_policy = new ComplexValueItem({
type: "daily",
parameters: {
daily_time: 0
}
}, true);
}
}

View File

@ -0,0 +1,19 @@
import { Type } from '@angular/core';
import { ReplicationConfigComponent } from './replication/replication-config.component';
import { SystemSettingsComponent } from './system/system-settings.component';
import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component';
import { RegistryConfigComponent } from './registry-config.component';
export * from './config';
export * from './replication/replication-config.component';
export * from './system/system-settings.component';
export * from './vulnerability/vulnerability-config.component';
export * from './registry-config.component';
export const CONFIGURATION_DIRECTIVES: Type<any>[] = [
ReplicationConfigComponent,
SystemSettingsComponent,
VulnerabilityConfigComponent,
RegistryConfigComponent
];

View File

@ -0,0 +1,7 @@
export const REGISTRY_CONFIG_HTML: string = `
<div>
<replication-config [(replicationConfig)]="config"></replication-config>
<system-settings [(systemSettings)]="config"></system-settings>
<vulnerability-config [(vulnerabilityConfig)]="config"></vulnerability-config>
</div>
`;

View File

@ -0,0 +1,98 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { SharedModule } from '../shared/shared.module';
import { ErrorHandler } from '../error-handler/error-handler';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { ReplicationConfigComponent } from './replication/replication-config.component';
import { SystemSettingsComponent } from './system/system-settings.component';
import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component';
import { RegistryConfigComponent } from './registry-config.component';
import {
ConfigurationService,
ConfigurationDefaultService,
ScanningResultService,
ScanningResultDefaultService
} from '../service/index';
import { Configuration } from './config';
describe('RegistryConfigComponent (inline template)', () => {
let comp: RegistryConfigComponent;
let fixture: ComponentFixture<RegistryConfigComponent>;
let cfgService: ConfigurationService;
let spy: jasmine.Spy;
let saveSpy: jasmine.Spy;
let mockConfig: Configuration = new Configuration();
mockConfig.token_expiration.value = 90;
mockConfig.verify_remote_cert.value = true;
mockConfig.scan_all_policy.value = {
type: "daily",
parameters: {
daily_time: 0
}
};
let config: IServiceConfig = {
configurationEndpoint: '/api/configurations/testing'
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
declarations: [
ReplicationConfigComponent,
SystemSettingsComponent,
VulnerabilityConfigComponent,
RegistryConfigComponent
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: ConfigurationService, useClass: ConfigurationDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(RegistryConfigComponent);
comp = fixture.componentInstance;
cfgService = fixture.debugElement.injector.get(ConfigurationService);
spy = spyOn(cfgService, 'getConfigurations').and.returnValue(Promise.resolve(mockConfig));
saveSpy = spyOn(cfgService, 'saveConfigurations').and.returnValue(Promise.resolve(true));
fixture.detectChanges();
});
it('should render configurations to the view', async(() => {
expect(spy.calls.count()).toEqual(1);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLInputElement = fixture.nativeElement.querySelector('input[type="text"]');
expect(el).toBeTruthy();
expect(el.value).toEqual('30');
let el2: HTMLInputElement = fixture.nativeElement.querySelector('input[type="checkbox"]');
expect(el2).toBeTruthy();
expect(el2.value).toEqual('on');
let el3: HTMLInputElement = fixture.nativeElement.querySelector('input[type="time"]');
expect(el3).toBeTruthy();
expect(el3.value).toEqual("08:00");
});
}));
it('should save the configuration changes', async(() => {
comp.save();
fixture.detectChanges();
expect(saveSpy.calls.any).toBeTruthy();
}));
});

View File

@ -0,0 +1,110 @@
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
import { Configuration, ComplexValueItem } from './config';
import { REGISTRY_CONFIG_HTML } from './registry-config.component.html';
import { ConfigurationService } from '../service/index';
import { toPromise } from '../utils';
import { ErrorHandler } from '../error-handler';
@Component({
selector: 'hbr-registry-config',
template: REGISTRY_CONFIG_HTML
})
export class RegistryConfigComponent implements OnInit {
config: Configuration = new Configuration();
configCopy: Configuration;
@Output() configChanged: EventEmitter<any> = new EventEmitter<any>();
constructor(
private configService: ConfigurationService,
private errorHandler: ErrorHandler
) { }
ngOnInit(): void {
//Initialize
this.load();
}
//Load configurations
load(): void {
toPromise<Configuration>(this.configService.getConfigurations())
.then((config: Configuration) => {
this.configCopy = Object.assign({}, config);
this.config = config;
})
.catch(error => this.errorHandler.error(error));
}
//Save configuration changes
save(): void {
let changes: { [key: string]: any | any[] } = this.getChanges();
if (this._isEmptyObject(changes)) {
//Guard code, do nothing
return;
}
//Fix policy parameters issue
let scanningAllPolicy = changes["scan_all_policy"];
if (scanningAllPolicy &&
scanningAllPolicy.type !== "daily" &&
scanningAllPolicy.parameters) {
delete (scanningAllPolicy.parameters);
}
toPromise<any>(this.configService.saveConfigurations(changes))
.then(() => {
this.configChanged.emit(changes);
})
.catch(error => this.errorHandler.error(error));
}
reset(): void {
//Reset to the values of copy
let changes: { [key: string]: any | any[] } = this.getChanges();
for (let prop in changes) {
this.config[prop] = Object.assign({}, this.configCopy[prop]);
}
}
getChanges(): { [key: string]: any | any[] } {
let changes: { [key: string]: any | any[] } = {};
if (!this.config || !this.configCopy) {
return changes;
}
for (let prop in this.config) {
let field = this.configCopy[prop];
if (field && field.editable) {
if (!this._compareValue(field.value, this.config[prop].value)) {
changes[prop] = this.config[prop].value;
//Number
if (typeof field.value === "number") {
changes[prop] = +changes[prop];
}
//Trim string value
if (typeof field.value === "string") {
changes[prop] = ('' + changes[prop]).trim();
}
}
}
}
return changes;
}
//private
_compareValue(a: any, b: any): boolean {
if ((a && !b) || (!a && b)) return false;
if (!a && !b) return true;
return JSON.stringify(a) === JSON.stringify(b);
}
//private
_isEmptyObject(obj: any): boolean {
return !obj || JSON.stringify(obj) === "{}";
}
}

View File

@ -0,0 +1,16 @@
export const REPLICATION_CONFIG_HTML: string = `
<form #replicationConfigFrom="ngForm" class="compact">
<section class="form-block" style="margin-top:0px;margin-bottom:0px;">
<label style="font-size:14px;font-weight:600;">Image Replication</label>
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="replicationConfig.verify_remote_cert.value" [disabled]="!editable">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
`;

View File

@ -0,0 +1,35 @@
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { REPLICATION_CONFIG_HTML } from './replication-config.component.html';
import { Configuration } from '../config';
@Component({
selector: 'replication-config',
template: REPLICATION_CONFIG_HTML
})
export class ReplicationConfigComponent {
config: Configuration;
@Output() configChange: EventEmitter<Configuration> = new EventEmitter<Configuration>();
@Input()
get replicationConfig(): Configuration {
return this.config;
}
set replicationConfig(cfg: Configuration) {
this.config = cfg;
this.configChange.emit(this.config);
}
@ViewChild("replicationConfigFrom") replicationConfigForm: NgForm;
get editable(): boolean {
return this.replicationConfig &&
this.replicationConfig.verify_remote_cert &&
this.replicationConfig.verify_remote_cert.editable;
}
get isValid(): boolean {
return this.replicationConfigForm && this.replicationConfigForm.valid;
}
}

View File

@ -0,0 +1,24 @@
export const SYSTEM_SETTINGS_HTML: string = `
<form #systemConfigFrom="ngForm" class="compact">
<section class="form-block" style="margin-top:0px;margin-bottom:0px;">
<label style="font-size:14px;font-weight:600;">System Settings</label>
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="systemSettings.token_expiration.value"
required
pattern="^[1-9]{1}[0-9]*$"
id="tokenExpiration"
size="20" [disabled]="!editable">
<span class="tooltip-content">
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>
`;

View File

@ -0,0 +1,35 @@
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { SYSTEM_SETTINGS_HTML } from './system-settings.component.html';
import { Configuration } from '../config';
@Component({
selector: 'system-settings',
template: SYSTEM_SETTINGS_HTML
})
export class SystemSettingsComponent {
config: Configuration;
@Output() configChange: EventEmitter<Configuration> = new EventEmitter<Configuration>();
@Input()
get systemSettings(): Configuration {
return this.config;
}
set systemSettings(cfg: Configuration) {
this.config = cfg;
this.configChange.emit(this.config);
}
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
get editable(): boolean {
return this.systemSettings &&
this.systemSettings.token_expiration &&
this.systemSettings.token_expiration.editable;
}
get isValid(): boolean {
return this.systemSettingsForm && this.systemSettingsForm.valid;
}
}

View File

@ -0,0 +1,32 @@
export const VULNERABILITY_CONFIG_HTML: string = `
<form #systemConfigFrom="ngForm" class="compact">
<section class="form-block" style="margin-top:0px;margin-bottom:0px;">
<label class="section-title">{{ 'CONFIG.SCANNING.TITLE' | translate }}</label>
<div class="form-group">
<label for="scanAllPolicy">{{ 'CONFIG.SCANNING.SCAN_ALL' | translate }}</label>
<div class="select">
<select id="scanAllPolicy" name="scanAllPolicy" [disabled]="!editable" [(ngModel)]="vulnerabilityConfig.scan_all_policy.value.type">
<option value="none">{{ 'CONFIG.SCANNING.NONE_POLICY' | translate }}</option>
<option value="daily">{{ 'CONFIG.SCANNING.DAILY_POLICY' | translate }}</option>
<option value="on_refresh">{{ 'CONFIG.SCANNING.REFRESH_POLICY' | translate }}</option>
</select>
</div>
<input type="time" name="dailyTimePicker" [disabled]="!editable" [hidden]="!showTimePicker" [(ngModel)]="dailyTime" />
</div>
<div class="form-group form-group-override">
<button class="btn btn-primary btn-sm" style="width:160px;" (click)="scanNow()">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</button>
</div>
</section>
</form>
`;
export const VULNERABILITY_CONFIG_STYLES: string = `
.form-group-override {
padding-left: 0px !important;
}
.section-title {
font-size: 14px !important;
font-weight: 600 !important;
}
`;

View File

@ -0,0 +1,165 @@
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Configuration } from '../config';
import { VULNERABILITY_CONFIG_HTML, VULNERABILITY_CONFIG_STYLES } from './vulnerability-config.component.template';
import { ScanningResultService } from '../../service/scanning.service';
import { ErrorHandler } from '../../error-handler';
import { toPromise } from '../../utils';
import { TranslateService } from '@ngx-translate/core';
const ONE_HOUR_SECONDS: number = 3600;
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
@Component({
selector: 'vulnerability-config',
template: VULNERABILITY_CONFIG_HTML,
styles: [VULNERABILITY_CONFIG_STYLES]
})
export class VulnerabilityConfigComponent {
_localTime: Date = new Date();
config: Configuration;
@Output() configChange: EventEmitter<Configuration> = new EventEmitter<Configuration>();
@Input()
get vulnerabilityConfig(): Configuration {
return this.config;
}
set vulnerabilityConfig(cfg: Configuration) {
this.config = cfg;
if (this.config.scan_all_policy &&
this.config.scan_all_policy.value) {
if (this.config.scan_all_policy.value.type === "daily"){
if(!this.config.scan_all_policy.value.parameters){
this.config.scan_all_policy.value.parameters = {
daily_time: 0
};
}
}
}
this.configChange.emit(this.config);
}
//UTC time
get dailyTime(): string {
if (!(this.config &&
this.config.scan_all_policy &&
this.config.scan_all_policy.value &&
this.config.scan_all_policy.value.type === "daily")) {
return "00:00";
}
let timeOffset: number = 0;//seconds
if (this.config.scan_all_policy.value.parameters) {
let daily_time = this.config.scan_all_policy.value.parameters.daily_time;
if (daily_time && typeof daily_time === "number") {
timeOffset = +daily_time;
}
}
//Convert to current time
let timezoneOffset: number = this._localTime.getTimezoneOffset();
//Local time
timeOffset = timeOffset - timezoneOffset * 60;
if (timeOffset < 0) {
timeOffset = timeOffset + ONE_DAY_SECONDS;
}
if (timeOffset >= ONE_DAY_SECONDS) {
timeOffset -= ONE_DAY_SECONDS;
}
//To time string
let hours: number = Math.floor(timeOffset / ONE_HOUR_SECONDS);
let minutes: number = Math.floor((timeOffset - hours * ONE_HOUR_SECONDS) / 60);
let timeStr: string = "" + hours;
if (hours < 10) {
timeStr = "0" + timeStr;
}
if (minutes < 10) {
timeStr += ":0";
} else {
timeStr += ":";
}
timeStr += minutes;
return timeStr;
}
set dailyTime(v: string) {
if (!v || v === "") {
return;
}
if (!(this.config &&
this.config.scan_all_policy &&
this.config.scan_all_policy.value &&
this.config.scan_all_policy.value.type === "daily")) {
return;
}
if (!this.config.scan_all_policy.value.parameters) {
this.config.scan_all_policy.value.parameters = {
daily_time: 0
};
}
let values: string[] = v.split(":");
if (!values || values.length !== 2) {
return;
}
let hours: number = +values[0];
let minutes: number = +values[1];
//Convert to UTC time
let timezoneOffset: number = this._localTime.getTimezoneOffset();
let utcTimes: number = hours * ONE_HOUR_SECONDS + minutes * 60;
utcTimes += timezoneOffset * 60;
if (utcTimes < 0) {
utcTimes += ONE_DAY_SECONDS;
}
if (utcTimes >= ONE_DAY_SECONDS) {
utcTimes -= ONE_DAY_SECONDS;
}
this.config.scan_all_policy.value.parameters.daily_time = utcTimes;
}
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
get editable(): boolean {
return this.vulnerabilityConfig &&
this.vulnerabilityConfig.scan_all_policy &&
this.vulnerabilityConfig.scan_all_policy.editable;
}
get isValid(): boolean {
return this.systemSettingsForm && this.systemSettingsForm.valid;
}
get showTimePicker(): boolean {
return this.vulnerabilityConfig &&
this.vulnerabilityConfig.scan_all_policy &&
this.vulnerabilityConfig.scan_all_policy.value &&
this.vulnerabilityConfig.scan_all_policy.value.type === "daily";
}
constructor(
private scanningService: ScanningResultService,
private errorHandler: ErrorHandler,
private translate: TranslateService) { }
scanNow(): void {
toPromise<any>(this.scanningService.startScanningAll())
.then(() => {
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_SUCCESS").subscribe((res: string) => {
this.errorHandler.info(res);
});
//TODO:
//Change button disable status.
})
.catch(error => this.errorHandler.error(error))
}
}

View File

@ -22,6 +22,7 @@ import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index';
import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index';
import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index';
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index';
import { CONFIGURATION_DIRECTIVES } from './config/index';
import {
SystemInfoService,
@ -37,7 +38,9 @@ import {
TagService,
TagDefaultService,
ScanningResultService,
ScanningResultDefaultService
ScanningResultDefaultService,
ConfigurationService,
ConfigurationDefaultService
} from './service/index';
import {
ErrorHandler,
@ -68,7 +71,8 @@ export const DefaultServiceConfig: IServiceConfig = {
langMessageLoader: "local",
langMessagePathForHttpLoader: "i18n/langs/",
langMessageFileSuffixForHttpLoader: "-lang.json",
localI18nMessageVariableMap: {}
localI18nMessageVariableMap: {},
configurationEndpoint: "/api/configurations"
};
/**
@ -103,7 +107,10 @@ export interface HarborModuleConfig {
tagService?: Provider,
//Service implementation for vulnerability scanning
scanningService?: Provider
scanningService?: Provider,
//Service implementation for configuration
configService?: Provider
}
/**
@ -145,7 +152,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
CREATE_EDIT_RULE_DIRECTIVES,
DATETIME_PICKER_DIRECTIVES,
VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES
PUSH_IMAGE_BUTTON_DIRECTIVES,
CONFIGURATION_DIRECTIVES
],
exports: [
LOG_DIRECTIVES,
@ -164,6 +172,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
DATETIME_PICKER_DIRECTIVES,
VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES,
CONFIGURATION_DIRECTIVES,
TranslateModule
],
providers: []
@ -183,6 +192,7 @@ export class HarborLibraryModule {
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
config.tagService || { provide: TagService, useClass: TagDefaultService },
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
//Do initializing
TranslateServiceInitializer,
{
@ -208,6 +218,7 @@ export class HarborLibraryModule {
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
config.tagService || { provide: TagService, useClass: TagDefaultService },
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }
]
};
}

View File

@ -15,3 +15,4 @@ export * from './vulnerability-scanning/index';
export * from './i18n/index';
export * from './push-image/index';
export * from './third-party/index';
export * from './config/index';

View File

@ -172,4 +172,12 @@ export interface IServiceConfig {
* @memberOf IServiceConfig
*/
localI18nMessageVariableMap?: { [key: string]: any };
/**
* The base endpoint of configuration service.
*
* @type {string}
* @memberOf IServiceConfig
*/
configurationEndpoint?: string;
}

View File

@ -0,0 +1,42 @@
import { TestBed, inject } from '@angular/core/testing';
import { ConfigurationService, ConfigurationDefaultService } from './configuration.service';
import { SharedModule } from '../shared/shared.module';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
describe('ConfigurationService', () => {
const mockConfig: IServiceConfig = {
configurationEndpoint: "/api/configurations/testing"
};
let config: IServiceConfig;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
providers: [
ConfigurationDefaultService,
{
provide: ConfigurationService,
useClass: ConfigurationDefaultService
}, {
provide: SERVICE_CONFIG,
useValue: mockConfig
}]
});
config = TestBed.get(SERVICE_CONFIG);
});
it('should be initialized', inject([ConfigurationDefaultService], (service: ConfigurationService) => {
expect(service).toBeTruthy();
}));
it('should inject the right config', () => {
expect(config).toBeTruthy();
expect(config.configurationEndpoint).toEqual("/api/configurations/testing");
});
});

View File

@ -0,0 +1,69 @@
import { Observable } from 'rxjs/Observable';
import { Injectable, Inject } from "@angular/core";
import 'rxjs/add/observable/of';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Http } from '@angular/http';
import { HTTP_JSON_OPTIONS } from '../utils';
import { Configuration } from '../config/config'
/**
* Service used to get and save registry-related configurations.
*
* @export
* @abstract
* @class ConfigurationService
*/
export abstract class ConfigurationService {
/**
* Get configurations.
*
* @abstract
* @returns {(Observable<Configuration> | Promise<Configuration> | Configuration)}
*
* @memberOf ConfigurationService
*/
abstract getConfigurations(): Observable<Configuration> | Promise<Configuration> | Configuration;
/**
* Save configurations.
*
* @abstract
* @returns {(Observable<Configuration> | Promise<Configuration> | Configuration)}
*
* @memberOf ConfigurationService
*/
abstract saveConfigurations(changedConfigs: any | { [key: string]: any | any[] }): Observable<any> | Promise<any> | any;
}
@Injectable()
export class ConfigurationDefaultService extends ConfigurationService {
_baseUrl: string;
constructor(
private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig) {
super();
this._baseUrl = this.config && this.config.configurationEndpoint ?
this.config.configurationEndpoint : "/api/configurations";
}
getConfigurations(): Observable<Configuration> | Promise<Configuration> | Configuration {
return this.http.get(this._baseUrl, HTTP_JSON_OPTIONS).toPromise()
.then(response => response.json() as Configuration)
.catch(error => Promise.reject(error));
}
saveConfigurations(changedConfigs: any | { [key: string]: any | any[] }): Observable<any> | Promise<any> | any {
if (!changedConfigs) {
return Promise.reject("Bad argument!");
}
return this.http.put(this._baseUrl, JSON.stringify(changedConfigs), HTTP_JSON_OPTIONS)
.toPromise()
.then(() => { })
.catch(error => Promise.reject(error));
}
}

View File

@ -7,3 +7,4 @@ export * from './repository.service';
export * from './tag.service';
export * from './RequestQueryParams';
export * from './scanning.service';
export * from './configuration.service';

View File

@ -53,6 +53,16 @@ export abstract class ScanningResultService {
* @memberOf ScanningResultService
*/
abstract startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any;
/**
* Trigger the scanning all action.
*
* @abstract
* @returns {(Observable<any> | Promise<any> | any)}
*
* @memberOf ScanningResultService
*/
abstract startScanningAll(): Observable<any> | Promise<any> | any;
}
@Injectable()
@ -95,4 +105,10 @@ export class ScanningResultDefaultService extends ScanningResultService {
.then(() => { return true })
.catch(error => Promise.reject(error));
}
startScanningAll(): Observable<any> | Promise<any> | any {
return this.http.post(`${this._baseUrl}/scanAll`,{}).toPromise()
.then(() => {return true})
.catch(error => Promise.reject(error));
}
}

View File

@ -117,6 +117,10 @@ export class ResultTipComponent implements OnInit {
return "VULNERABILITY.SINGULAR";
}
packageText(count: number): string {
return count > 1 ? "VULNERABILITY.PACKAGES" : "VULNERABILITY.PACKAGE";
}
public get completeTimestamp(): Date {
return this.summary && this.summary.update_time ? this.summary.update_time : new Date();
}

View File

@ -13,23 +13,23 @@ export const TIP_COMPONENT_HTML: string = `
<div class="bar-summary bar-tooltip-fon">
<div *ngIf="hasHigh" class="bar-summary-item">
<clr-icon shape="exclamation-circle" class="is-error" size="24"></clr-icon>
<span>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}</span>
<span>{{highCount}} {{packageText(highCount) | translate }} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}</span>
</div>
<div *ngIf="hasMedium" class="bar-summary-item">
<clr-icon *ngIf="hasMedium" shape="exclamation-triangle" class="is-warning" size="24"></clr-icon>
<span>{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }}</span>
<span>{{mediumCount}} {{packageText(mediumCount) | translate }} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }}</span>
</div>
<div *ngIf="hasLow" class="bar-summary-item">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }}</span>
<span>{{lowCount}} {{packageText(lowCount) | translate }} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }}</span>
</div>
<div *ngIf="hasUnknown" class="bar-summary-item">
<clr-icon shape="help" size="24"></clr-icon>
<span>{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }}</span>
<span>{{unknownCount}} {{packageText(unknownCount) | translate }} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }}</span>
</div>
<div *ngIf="hasNone" class="bar-summary-item">
<clr-icon shape="check-circle" class="is-success" size="24"></clr-icon>
<span>{{noneCount}} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }}</span>
<span>{{noneCount}} {{packageText(noneCount) | translate }} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }}</span>
</div>
</div>
<div>

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.9.8",
"clarity-ui": "^0.9.8",
"core-js": "^2.4.1",
"harbor-ui": "^0.2.25",
"harbor-ui": "^0.2.40",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -15,7 +15,7 @@ import { Component, Input, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Subscription } from 'rxjs/Subscription';
import { Configuration } from '../config';
import { Configuration } from 'harbor-ui';
@Component({
selector: 'config-auth',

View File

@ -15,50 +15,24 @@
<li role="presentation" class="nav-item">
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>Vulnerability</button>
</li>
</ul>
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>
<config-auth [ldapConfig]="allConfig"></config-auth>
</section>
<section id="replication" role="tabpanel" aria-labelledby="config-replication" [hidden]='!isCurrentTabContent("replication")'>
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
<replication-config [(replicationConfig)]="allConfig"></replication-config>
</section>
<section id="email" role="tabpanel" aria-labelledby="config-email" [hidden]='!isCurrentTabContent("email")'>
<config-email [mailConfig]="allConfig"></config-email>
</section>
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="allConfig.token_expiration.value"
required
pattern="^[1-9]{1}[\d]*$"
id="tokenExpiration"
size="40" [disabled]="disabled(allConfig.token_expiration)">
<span class="tooltip-content">
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
<system-settings [(systemSettings)]="allConfig"></system-settings>
</section>
</form>
<section id="vulnerability" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
</section>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
@ -67,6 +41,7 @@
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span>
<span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideLDAPTestingSpinner"></span>
<button type="button" class="btn btn-primary" (click)="consoleTest()">CONSOLE</button>
</div>
</div>
</div>

View File

@ -13,12 +13,9 @@
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { NgForm } from '@angular/forms';
import { ConfigurationService } from './config.service';
import { Configuration } from './config';
import { ConfirmationTargets, ConfirmationState } from '../shared/shared.const';;
import { StringValueItem } from './config';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { Subscription } from 'rxjs/Subscription';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message'
@ -29,13 +26,22 @@ import { ConfigurationEmailComponent } from './email/config-email.component';
import { AppConfigService } from '../app-config.service';
import { SessionService } from '../shared/session.service';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import {
Configuration,
StringValueItem,
ComplexValueItem,
ReplicationConfigComponent,
SystemSettingsComponent,
VulnerabilityConfigComponent
} from 'harbor-ui';
const fakePass = "aWpLOSYkIzJTTU4wMDkx";
const TabLinkContentMap = {
"config-auth": "authentication",
"config-replication": "replication",
"config-email": "email",
"config-system": "system_settings"
"config-system": "system_settings",
"config-vulnerability": "vulnerability"
};
@Component({
@ -52,8 +58,9 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
testingMailOnGoing: boolean = false;
testingLDAPOnGoing: boolean = false;
@ViewChild("repoConfigFrom") repoConfigForm: NgForm;
@ViewChild("systemConfigFrom") systemConfigForm: NgForm;
@ViewChild(ReplicationConfigComponent) replicationConfig: ReplicationConfigComponent;
@ViewChild(SystemSettingsComponent) systemSettingsConfig: SystemSettingsComponent;
@ViewChild(VulnerabilityConfigComponent) vulnerabilityConfig: VulnerabilityConfigComponent;
@ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent;
@ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent;
@ -64,6 +71,11 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
private appConfigService: AppConfigService,
private session: SessionService) { }
consoleTest(): void {
console.log(this.allConfig, this.originalCopy);
console.log("-------------");
console.log(this.getChanges());
}
isCurrentTabLink(tabId: string): boolean {
return this.currentTabId === tabId;
}
@ -101,6 +113,9 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
case "config-system":
properties = ["token_expiration"];
break;
case "config-vulnerability":
properties = ["scan_all_policy"];
break;
default:
return null;
}
@ -146,10 +161,12 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
}
public isValid(): boolean {
return this.repoConfigForm &&
this.repoConfigForm.valid &&
this.systemConfigForm &&
this.systemConfigForm.valid &&
return this.replicationConfig &&
this.replicationConfig.isValid &&
this.systemSettingsConfig &&
this.systemSettingsConfig.isValid &&
this.vulnerabilityConfig &&
this.vulnerabilityConfig.isValid &&
this.mailConfig &&
this.mailConfig.isValid() &&
this.authConfig &&
@ -191,7 +208,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
}
public tabLinkClick(tabLink: string) {
//Whether has unsave changes in current tab
//Whether has unsaved changes in current tab
let changes = this.hasUnsavedChangesOfCurrentTab();
if (!changes) {
this.currentTabId = tabLink;
@ -210,6 +227,14 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
public save(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
//Fix policy parameters issue
let scanningAllPolicy = changes["scan_all_policy"];
if (scanningAllPolicy &&
scanningAllPolicy.type !== "daily" &&
scanningAllPolicy.parameters) {
delete (scanningAllPolicy.parameters);
}
this.onGoing = true;
this.configService.saveConfiguration(changes)
.then(response => {
@ -247,7 +272,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
if (!this.isEmpty(changes)) {
this.confirmUnsavedChanges(changes);
} else {
//Inprop situation, should not come here
//Invalid situation, should not come here
console.error("Nothing changed");
}
}
@ -364,14 +389,14 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()
.then(configurations => {
.then((configurations: Configuration) => {
this.onGoing = false;
//Add two password fields
configurations.email_password = new StringValueItem(fakePass, true);
configurations.ldap_search_password = new StringValueItem(fakePass, true);
this.allConfig = configurations;
this.allConfig = configurations;
//Keep the original copy of the data
this.originalCopy = this.clone(configurations);
})
@ -390,8 +415,8 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
*
* @memberOf ConfigurationComponent
*/
getChanges(): any {
let changes = {};
getChanges(): { [key: string]: any | any[] } {
let changes: { [key: string]: any | any[] } = {};
if (!this.allConfig || !this.originalCopy) {
return changes;
}
@ -399,11 +424,11 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
for (let prop in this.allConfig) {
let field = this.originalCopy[prop];
if (field && field.editable) {
if (field.value != this.allConfig[prop].value) {
if (!this.compareValue(field.value, this.allConfig[prop].value)) {
changes[prop] = this.allConfig[prop].value;
//Fix boolean issue
if (typeof field.value === "boolean") {
changes[prop] = changes[prop] ? "1" : "0";
//Number
if (typeof field.value === "number") {
changes[prop] = +changes[prop];
}
//Trim string value
@ -417,6 +442,19 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return changes;
}
//private
compareValue(a: any, b: any): boolean {
if ((a && !b) || (!a && b)) return false;
if (!a && !b) return true;
return JSON.stringify(a) === JSON.stringify(b);
}
//private
isEmpty(obj: any): boolean {
return !obj || JSON.stringify(obj) === "{}";
}
/**
*
* Deep clone the configuration object
@ -428,18 +466,11 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
* @memberOf ConfigurationComponent
*/
clone(src: Configuration): Configuration {
let dest = new Configuration();
if (!src) {
return dest;//Empty
return new Configuration();//Empty
}
for (let prop in src) {
if (src[prop]) {
dest[prop] = Object.assign({}, src[prop]); //Deep copy inner object
}
}
return dest;
return JSON.parse(JSON.stringify(src));
}
/**
@ -464,14 +495,6 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
}
}
isEmpty(obj: any) {
for (let key in obj) {
if (obj.hasOwnProperty(key))
return false;
}
return true;
}
disabled(prop: any): boolean {
return !(prop && prop.editable);
}

View File

@ -15,7 +15,7 @@ import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Configuration } from './config';
import { Configuration } from 'harbor-ui';
const configEndpoint = "/api/configurations";
const emailEndpoint = "/api/email/ping";

View File

@ -14,7 +14,7 @@
import { Component, Input, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Configuration } from '../config';
import { Configuration } from 'harbor-ui';
@Component({
selector: 'config-email',

View File

@ -403,6 +403,15 @@
"UID": "LDAP UID",
"SCOPE": "LDAP Scope"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
"TITLE": "Vulnerability Scanning",
"SCAN_ALL": "Scan All",
"SCAN_NOW": "SCAN NOW",
"NONE_POLICY": "None",
"DAILY_POLICY": "Daily At",
"REFRESH_POLICY": "Upon Refresh"
},
"TEST_MAIL_SUCCESS": "Connection to mail server is verified.",
"TEST_LDAP_SUCCESS": "Connection to LDAP server is verified.",
"TEST_MAIL_FAILED": "Failed to verify mail server with error: {{param}}.",

View File

@ -404,6 +404,15 @@
"UID": "LDAP UID",
"SCOPE": "LDAP Ámbito"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
"TITLE": "Vulnerability Scanning",
"SCAN_ALL": "Scan All",
"SCAN_NOW": "SCAN NOW",
"NONE_POLICY": "None",
"DAILY_POLICY": "Daily At",
"REFRESH_POLICY": "Upon Refresh"
},
"TEST_MAIL_SUCCESS": "La conexión al servidor de correo ha sido verificada.",
"TEST_LDAP_SUCCESS": "La conexión al servidor LDAP ha sido verificada.",
"TEST_MAIL_FAILED": "Fallo al verificar el servidor de correo con el error: {{param}}.",

View File

@ -403,6 +403,15 @@
"UID": "LDAP用户UID的属性",
"SCOPE": "LDAP搜索范围"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "成功启动扫描所有镜像任务!",
"TITLE": "缺陷扫描",
"SCAN_ALL": "扫描所有",
"SCAN_NOW": "开始扫描",
"NONE_POLICY": "无",
"DAILY_POLICY": "每日",
"REFRESH_POLICY": "缺陷库刷新后"
},
"TEST_MAIL_SUCCESS": "邮件服务器的连通正常。",
"TEST_LDAP_SUCCESS": "LDAP服务器的连通正常。",
"TEST_MAIL_FAILED": "验证邮件服务器失败,错误: {{param}}。",