Add permission check to CVE export (#17267)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2022-07-29 19:48:39 +08:00 committed by GitHub
parent 04fa3853c9
commit 7e7ae7ea1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 324 additions and 147 deletions

View File

@ -102,7 +102,7 @@
<clr-dropdown class="width-tag-label">
<div class="label-text">
<div
class="dropdown-toggle"
class="dropdown-toggle labels"
clrDropdownTrigger>
<ng-container
*ngFor="

View File

@ -78,3 +78,8 @@
.input-width {
width: 310px;
}
.labels {
display: flex;
justify-content: left;
}

View File

@ -1,7 +1,11 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import {
Component,
ElementRef,
EventEmitter,
Output,
ViewChild,
} from '@angular/core';
import { Label } from 'ng-swagger-gen/models/label';
import { LabelService } from 'ng-swagger-gen/services/label.service';
import { forkJoin, Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { Project } from 'src/app/base/project/project';
import { NgForm } from '@angular/forms';
@ -13,8 +17,8 @@ import {
EventService,
HarborEvent,
} from '../../../../../services/event-service/event.service';
import { LabelService } from 'src/app/shared/services/label.service';
const PAGE_SIZE: number = 100;
const SUPPORTED_MIME_TYPE: string =
'application/vnd.security.vulnerability.report; version=1.1';
@Component({
@ -23,6 +27,7 @@ const SUPPORTED_MIME_TYPE: string =
styleUrls: ['./export-cve.component.scss'],
})
export class ExportCveComponent {
@Output() triggerExportSuccess = new EventEmitter<void>();
selectedProjects: Project[] = [];
opened: boolean = false;
loading: boolean = false;
@ -92,6 +97,7 @@ export class ExportCveComponent {
)
.subscribe(
res => {
this.triggerExportSuccess.emit();
this.msgHandler.showSuccess(
'CVE_EXPORT.TRIGGER_EXPORT_SUCCESS'
);
@ -107,7 +113,7 @@ export class ExportCveComponent {
isSelected(l: Label): boolean {
let flag: boolean = false;
this.selectedLabels.forEach(item => {
if (item.name === l.name) {
if (item.id === l.id) {
flag = true;
}
});
@ -116,7 +122,7 @@ export class ExportCveComponent {
selectOrUnselect(l: Label) {
if (this.isSelected(l)) {
this.selectedLabels = this.selectedLabels.filter(
item => item.name !== l.name
item => item.id !== l.id
);
} else {
this.selectedLabels.push(l);
@ -144,54 +150,12 @@ export class ExportCveComponent {
// get all global labels
this.loadingAllLabels = true;
this.labelService
.ListLabelsResponse({
pageSize: PAGE_SIZE,
page: 1,
scope: 'g',
})
.getAllGlobalAndSpecificProjectLabels(
this.selectedProjects[0].project_id
)
.pipe(finalize(() => (this.loadingAllLabels = false)))
.subscribe(res => {
if (res.headers) {
const xHeader: string = res.headers.get('X-Total-Count');
const totalCount = parseInt(xHeader, 0);
let arr = res.body || [];
if (totalCount <= 100) {
// already gotten all global labels
if (arr && arr.length) {
arr.forEach(data => {
this.allLabels.push(data);
});
}
} else {
// get all the global labels in specified times
const times: number = Math.ceil(totalCount / PAGE_SIZE);
const observableList: Observable<Label[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(
this.labelService.ListLabels({
page: i,
pageSize: PAGE_SIZE,
scope: 'g',
})
);
}
this.loadingAllLabels = true;
forkJoin(observableList)
.pipe(
finalize(() => (this.loadingAllLabels = false))
)
.subscribe(response => {
if (response && response.length) {
response.forEach(item => {
arr = arr.concat(item);
});
arr.forEach(data => {
this.allLabels.push(data);
});
}
});
}
}
this.allLabels = res;
});
}
handleBrace(originStr: string): string {

View File

@ -1,7 +1,8 @@
<clr-datagrid
(clrDgRefresh)="clrLoad($event)"
[clrDgLoading]="loading"
[(clrDgSelected)]="selectedRow">
[(clrDgSelected)]="selectedRow"
(clrDgSelectedChange)="selectionChanged()">
<clr-dg-action-bar>
<button
type="button"
@ -23,11 +24,14 @@
down"></clr-icon
></span>
<clr-dropdown-menu *clrIfOpen>
<button clrDropdownItem (click)="exportCVE()">
<button
[disabled]="!hasPermission || !canClickExport"
[clrLoading]="checkingPermission"
clrDropdownItem
(click)="exportCVE()">
<clr-icon shape="export" size="16"></clr-icon>&nbsp;
<span id="export-cve">{{
getExportButtonText()
| translate: { number: selectedRow?.length }
'CVE_EXPORT.EXPORT_SOME_PROJECTS' | translate
}}</span>
</button>
<div class="dropdown-divider"></div>
@ -102,4 +106,4 @@
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
<export-cve></export-cve>
<export-cve (triggerExportSuccess)="triggerExportSuccess()"></export-cve>

View File

@ -11,16 +11,21 @@
// 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 { Subscription, forkJoin, of } from 'rxjs';
import { forkJoin, Observable, of, Subscription } from 'rxjs';
import {
Component,
Output,
OnDestroy,
EventEmitter,
OnDestroy,
Output,
ViewChild,
} from '@angular/core';
import { Router } from '@angular/router';
import { ProjectService, State } from '../../../../shared/services';
import {
ProjectService,
State,
UserPermissionService,
USERSTATICPERMISSION,
} from '../../../../shared/services';
import { TranslateService } from '@ngx-translate/core';
import { SessionService } from '../../../../shared/services/session.service';
import { StatisticHandler } from '../statictics/statistic-handler.service';
@ -28,7 +33,7 @@ import { MessageHandlerService } from '../../../../shared/services/message-handl
import { SearchTriggerService } from '../../../../shared/components/global-search/search-trigger.service';
import { AppConfigService } from '../../../../services/app-config.service';
import { Project } from '../../../project/project';
import { map, catchError, finalize } from 'rxjs/operators';
import { catchError, finalize, map } from 'rxjs/operators';
import {
calculatePage,
getPageSizeFromLocalStorage,
@ -55,6 +60,8 @@ import { errorHandler } from '../../../../shared/units/shared.utils';
import { ConfirmationMessage } from '../../../global-confirmation-dialog/confirmation-message';
import { ExportCveComponent } from './export-cve/export-cve.component';
const MAX_PROJECTS_NUM: number = 1;
const INTERVAL: number = 30000;
@Component({
selector: 'list-project',
templateUrl: 'list-project.component.html',
@ -83,6 +90,9 @@ export class ListProjectComponent implements OnDestroy {
state: ClrDatagridStateInterface;
@ViewChild(ExportCveComponent)
exportCveComponent: ExportCveComponent;
hasPermission: boolean = false;
checkingPermission: boolean = false;
canClickExport: boolean = true;
constructor(
private session: SessionService,
private appConfigService: AppConfigService,
@ -94,7 +104,8 @@ export class ListProjectComponent implements OnDestroy {
private translate: TranslateService,
private deletionDialogService: ConfirmationDialogService,
private operationService: OperationService,
private translateService: TranslateService
private translateService: TranslateService,
private permissionService: UserPermissionService
) {
this.subscription =
deletionDialogService.confirmationConfirm$.subscribe(message => {
@ -382,11 +393,36 @@ export class ListProjectComponent implements OnDestroy {
exportCVE() {
this.exportCveComponent.open(this.selectedRow);
}
getExportButtonText(): string {
if (this.selectedRow?.length) {
return `CVE_EXPORT.EXPORT_SOME_PROJECTS`;
selectionChanged() {
this.hasPermission = false;
if (
this.selectedRow?.length &&
this.selectedRow?.length <= MAX_PROJECTS_NUM
) {
const obs: Observable<boolean>[] = [];
this.selectedRow.forEach(item => {
obs.push(
this.permissionService.getPermission(
item.project_id,
USERSTATICPERMISSION.EXPORT_CVE.KEY,
USERSTATICPERMISSION.EXPORT_CVE.VALUE.CREATE
)
);
});
this.checkingPermission = true;
forkJoin(obs)
.pipe(finalize(() => (this.checkingPermission = false)))
.subscribe(res => {
if (res?.length) {
this.hasPermission = res.every(item => item);
}
});
}
return 'CVE_EXPORT.EXPORT_ALL_PROJECTS';
}
triggerExportSuccess() {
this.canClickExport = false;
setTimeout(() => {
this.canClickExport = true;
}, INTERVAL);
}
}

View File

@ -281,31 +281,37 @@
class="dropdown clr-select-wrapper"
formArrayName="value">
<clr-dropdown class="width-tag-label">
<button
type="button"
class="width-100 dropdown-toggle btn btn-link statistic-data label-text"
<div
class="width-100 label-text"
clrDropdownTrigger>
<ng-template
ngFor
let-label
[ngForOf]="filter.value.value"
let-m="index">
<hbr-label-piece
*ngIf="m < 1"
[hasIcon]="false"
[label]="getLabel(label)"
[labelWidth]="
84
"></hbr-label-piece>
</ng-template>
<span
class="ellipsis color-white-dark"
*ngIf="
filter.value.value.length >
1
"
>···</span
>
<div class="label-container">
<ng-template
ngFor
let-label
[ngForOf]="
filter.value.value
"
let-m="index">
<hbr-label-piece
class="label-piece"
*ngIf="m < 1"
[hasIcon]="false"
[label]="
getLabel(label)
"
[labelWidth]="
84
"></hbr-label-piece>
</ng-template>
<span
class="ellipsis color-white-dark"
*ngIf="
filter.value.value
.length > 1
"
>···</span
>
</div>
<div
*ngFor="
let label1 of filter.value
@ -333,7 +339,7 @@
}}"
placeholder="select labels" />
</div>
</button>
</div>
<clr-dropdown-menu
[ngStyle]="{ 'max-height.px': 230 }"
class="right-align"

View File

@ -213,18 +213,12 @@ clr-modal {
}
.label-text {
text-transform: none;
letter-spacing: normal;
font-size: 13px;
font-weight: 400;
color: #000;
position: relative;
height: 1.2rem;
margin: 0 !important;
line-height: 1rem;
text-align: left;
padding-left: 6px;
outline: none;
border-bottom: 1px solid rgb(154 154 154);
display: flex;
align-items: center;
justify-content: left;
border-bottom: 0.05rem solid;
}
.ellipsis {
@ -311,3 +305,13 @@ clr-modal {
margin-left: 10px;
}
.label-piece {
display: flex;
left: .25rem;
}
.label-container{
position: absolute;
display: flex;
align-items: center;
}

View File

@ -8,9 +8,15 @@ import {
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { errorHandler } from '../shared/units/shared.utils';
export const SAFE_METHODS: string[] = ['GET', 'HEAD', 'OPTIONS', 'TRACE'];
enum INVALID_CSRF_TOKEN {
CODE = 403,
MESSAGE = 'CSRF token invalid',
}
@Injectable({
providedIn: 'root',
})
@ -75,7 +81,10 @@ export class InterceptHttpService implements HttpInterceptor {
})
);
}
if (error.status === 403) {
if (
error.status === INVALID_CSRF_TOKEN.CODE &&
errorHandler(error) === INVALID_CSRF_TOKEN.MESSAGE
) {
const csrfToken = localStorage.getItem('__csrf');
if (csrfToken) {
request = request.clone({

View File

@ -58,6 +58,7 @@ export class OperationComponent implements OnInit, OnDestroy {
}
}
timeout;
refreshExportJobSub: Subscription;
constructor(
private session: SessionService,
private operationService: OperationService,
@ -66,29 +67,34 @@ export class OperationComponent implements OnInit, OnDestroy {
private event: EventService,
private msgHandler: MessageHandlerService
) {
this.event.subscribe(HarborEvent.REFRESH_EXPORT_JOBS, () => {
if (this.animationState === 'out') {
this._newMessageCount += 1;
}
this.refreshExportJobs();
});
this.batchInfoSubscription = operationService.operationInfo$.subscribe(
data => {
if (this.animationState === 'out') {
this._newMessageCount += 1;
}
if (data) {
if (this.resultLists.length >= MAX_NUMBER) {
this.resultLists.splice(
MAX_NUMBER - 1,
this.resultLists.length + 1 - MAX_NUMBER
);
if (!this.refreshExportJobSub) {
this.refreshExportJobSub = this.event.subscribe(
HarborEvent.REFRESH_EXPORT_JOBS,
() => {
if (this.animationState === 'out') {
this._newMessageCount += 1;
}
this.resultLists.unshift(data);
this.refreshExportJobs();
}
}
);
);
}
if (!this.batchInfoSubscription) {
this.batchInfoSubscription =
operationService.operationInfo$.subscribe(data => {
if (this.animationState === 'out') {
this._newMessageCount += 1;
}
if (data) {
if (this.resultLists.length >= MAX_NUMBER) {
this.resultLists.splice(
MAX_NUMBER - 1,
this.resultLists.length + 1 - MAX_NUMBER
);
}
this.resultLists.unshift(data);
}
});
}
}
getNewMessageCountStr(): string {
@ -184,6 +190,7 @@ export class OperationComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
if (this.batchInfoSubscription) {
this.batchInfoSubscription.unsubscribe();
this.batchInfoSubscription = null;
}
if (this._timeoutInterval) {
clearInterval(this._timeoutInterval);
@ -193,6 +200,10 @@ export class OperationComponent implements OnInit, OnDestroy {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.refreshExportJobSub) {
this.refreshExportJobSub.unsubscribe();
this.refreshExportJobSub = null;
}
}
toggleTitle(errorSpan: any) {
@ -273,7 +284,7 @@ export class OperationComponent implements OnInit, OnDestroy {
hasFile: item.file_present,
name: `${FILE_NAME_PREFIX}${new HarborDatetimePipe().transform(
item.start_time,
'yyyyMMddHHss'
'yyyyMMddHHmmss'
)}`,
id: item.id,
errorInf:

View File

@ -3,13 +3,21 @@ import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { RequestQueryParams } from './RequestQueryParams';
import { Label } from './interface';
import { LabelService as GeneratedLabelService } from 'ng-swagger-gen/services/label.service';
import { Label as GeneratedLabel } from 'ng-swagger-gen/models/label';
import {
buildHttpRequestOptions,
CURRENT_BASE_HREF,
V1_BASE_HREF,
HTTP_JSON_OPTIONS,
} from '../units/utils';
import { Observable, throwError as observableThrowError } from 'rxjs';
import {
forkJoin,
mergeMap,
Observable,
of,
throwError as observableThrowError,
} from 'rxjs';
export abstract class LabelService {
abstract getGLabels(
@ -63,15 +71,22 @@ export abstract class LabelService {
version: string,
label: Label
): Observable<any>;
}
abstract getAllGlobalAndSpecificProjectLabels(
projectId: number
): Observable<GeneratedLabel[]>;
}
const PAGE_SIZE: number = 100;
@Injectable()
export class LabelDefaultService extends LabelService {
labelUrl: string;
chartUrl: string;
chartLabelUrl: string;
constructor(private http: HttpClient) {
constructor(
private http: HttpClient,
private labelService: GeneratedLabelService
) {
super();
this.labelUrl = CURRENT_BASE_HREF + '/labels';
this.chartUrl = V1_BASE_HREF + '/chartrepo';
@ -235,4 +250,119 @@ export class LabelDefaultService extends LabelService {
HTTP_JSON_OPTIONS
);
}
getAllGlobalAndSpecificProjectLabels(
projectId: number
): Observable<GeneratedLabel[]> {
return new Observable<GeneratedLabel[]>(observer => {
// get all project labels
forkJoin([
this._gelAllLabelsForGlobalOrProject(true, null),
this._gelAllLabelsForGlobalOrProject(false, projectId),
]).subscribe({
next: results => {
observer.next([].concat.apply([], results));
observer.complete();
},
error: err => {
observer.error(err);
observer.complete();
},
});
});
}
private _gelAllLabelsForGlobalOrProject(
isGlobal: boolean,
projectId: number
): Observable<GeneratedLabel[]> {
if (isGlobal) {
return this.labelService
.ListLabelsResponse({
pageSize: PAGE_SIZE,
page: 1,
scope: 'g',
})
.pipe(
mergeMap(res => {
if (res.headers) {
const xHeader: string =
res.headers.get('X-Total-Count');
const totalCount = parseInt(xHeader, 0);
if (totalCount <= PAGE_SIZE) {
return of(res.body);
} else {
// get all the project labels in specified times
const times: number = Math.ceil(
totalCount / PAGE_SIZE
);
const observableList: Observable<
GeneratedLabel[]
>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(
this.labelService.ListLabels({
page: i,
pageSize: PAGE_SIZE,
scope: 'g',
})
);
}
return new Observable<GeneratedLabel[]>(ob => {
forkJoin(observableList).subscribe(
labels => {
ob.next(
[].concat.apply([], labels)
);
}
);
});
}
}
})
);
}
return this.labelService
.ListLabelsResponse({
pageSize: PAGE_SIZE,
page: 1,
scope: 'p',
projectId: projectId,
})
.pipe(
mergeMap(res => {
if (res.headers) {
const xHeader: string =
res.headers.get('X-Total-Count');
const totalCount = parseInt(xHeader, 0);
if (totalCount <= PAGE_SIZE) {
return of(res.body);
} else {
// get all the project labels in specified times
const times: number = Math.ceil(
totalCount / PAGE_SIZE
);
const observableList: Observable<
GeneratedLabel[]
>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(
this.labelService.ListLabels({
page: i,
pageSize: PAGE_SIZE,
scope: 'p',
projectId: projectId,
})
);
}
return new Observable<GeneratedLabel[]>(ob => {
forkJoin(observableList).subscribe(labels => {
ob.next([].concat.apply([], labels));
});
});
}
}
})
);
}
}

View File

@ -202,4 +202,12 @@ export const USERSTATICPERMISSION = {
DELETE: 'delete',
},
},
EXPORT_CVE: {
KEY: 'export-cve',
VALUE: {
READ: 'read',
CREATE: 'create',
LIST: 'list',
},
},
};

View File

@ -332,3 +332,9 @@ hbr-copy-input {
.select-all-for-dropdown {
color: $select-all-for-dropdown-color !important;
}
hbr-create-edit-rule {
.label-text {
border-bottom-color: $normal-border-color !important;
}
}

View File

@ -44,4 +44,5 @@ $input-autofill-color: #eaedf0;
$pull-command-icon-color: #4aaed9;
$pull-command-icon-hover-color: #007CBB;
$select-all-for-dropdown-color: #4aaed9;
$normal-border-color: #acbac3;
@import "./common.scss";

View File

@ -45,4 +45,5 @@ $input-autofill-color: #000;
$pull-command-icon-color: #007CBB;
$pull-command-icon-hover-color: #4aaed9;
$select-all-for-dropdown-color: #0072a3;
$normal-border-color: #6a7a81;
@import "./common.scss";

View File

@ -1757,8 +1757,7 @@
"NO_PURGE_RECORDS": "We couldn't find any purge histories!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
"EXPORT_SOME_PROJECTS": "Export CVEs",
"ALL_PROJECTS": "All projects",
"EXPORT_TITLE": "Export CVE",
"EXPORT_SUBTITLE": "Set exporting conditions",

View File

@ -1757,8 +1757,7 @@
"NO_PURGE_RECORDS": "We couldn't find any purge histories!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
"EXPORT_SOME_PROJECTS": "Export CVEs",
"ALL_PROJECTS": "All projects",
"EXPORT_TITLE": "Export CVE",
"EXPORT_SUBTITLE": "Set exporting conditions",

View File

@ -1756,8 +1756,7 @@
"NO_PURGE_RECORDS": "We couldn't find any purge histories!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
"EXPORT_SOME_PROJECTS": "Export CVEs",
"ALL_PROJECTS": "All projects",
"EXPORT_TITLE": "Export CVE",
"EXPORT_SUBTITLE": "Set exporting conditions",

View File

@ -1726,8 +1726,7 @@
"NO_PURGE_RECORDS": "We couldn't find any purge histories!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
"EXPORT_SOME_PROJECTS": "Export CVEs",
"ALL_PROJECTS": "All projects",
"EXPORT_TITLE": "Export CVE",
"EXPORT_SUBTITLE": "Set exporting conditions",

View File

@ -1753,8 +1753,7 @@
"NO_PURGE_RECORDS": "We couldn't find any purge histories!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
"EXPORT_SOME_PROJECTS": "Export CVEs",
"ALL_PROJECTS": "All projects",
"EXPORT_TITLE": "Export CVE",
"EXPORT_SUBTITLE": "Set exporting conditions",

View File

@ -1757,8 +1757,7 @@
"NO_PURGE_RECORDS": "We couldn't find any purge histories!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
"EXPORT_SOME_PROJECTS": "Export CVEs",
"ALL_PROJECTS": "All projects",
"EXPORT_TITLE": "Export CVE",
"EXPORT_SUBTITLE": "Set exporting conditions",

View File

@ -1755,8 +1755,7 @@
"NO_PURGE_RECORDS": "未发现任何清理记录!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "导出 CVEs - {{number}} 个项目",
"EXPORT_ALL_PROJECTS": "导出 CVEs - 全部项目",
"EXPORT_SOME_PROJECTS": "导出 CVEs",
"ALL_PROJECTS": "全部项目",
"EXPORT_TITLE": "导出 CVE",
"EXPORT_SUBTITLE": "设置导出条件",

View File

@ -1748,8 +1748,7 @@
"NO_PURGE_RECORDS": "We couldn't find any purge histories!"
},
"CVE_EXPORT": {
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
"EXPORT_SOME_PROJECTS": "Export CVEs",
"ALL_PROJECTS": "All projects",
"EXPORT_TITLE": "Export CVE",
"EXPORT_SUBTITLE": "Set exporting conditions",