Merge latest updates.

This commit is contained in:
kunw 2017-06-02 16:53:03 +08:00
commit 4246f55180
14 changed files with 73 additions and 241 deletions

View File

@ -20,7 +20,6 @@ import (
"strconv"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
@ -91,20 +90,19 @@ var (
// ConfigAPI ...
type ConfigAPI struct {
api.BaseAPI
BaseController
}
// Prepare validates the user
func (c *ConfigAPI) Prepare() {
userID := c.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("failed to check the role of user: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
c.BaseController.Prepare()
if !c.SecurityCtx.IsAuthenticated() {
c.HandleUnauthorized()
return
}
if !isSysAdmin {
c.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
if !c.SecurityCtx.IsSysAdmin() {
c.HandleForbidden(c.SecurityCtx.GetUsername())
return
}
}

View File

@ -20,8 +20,6 @@ import (
"net/http"
"strings"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
"github.com/vmware/harbor/src/common/utils/log"
@ -29,25 +27,22 @@ import (
// LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import
type LdapAPI struct {
api.BaseAPI
BaseController
}
const metaChars = "&|!=~*<>()"
// Prepare ...
func (l *LdapAPI) Prepare() {
userID := l.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("error occurred in IsAdminRole: %v", err)
l.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
l.BaseController.Prepare()
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
if !isSysAdmin {
l.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
if !l.SecurityCtx.IsSysAdmin() {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
}
// Ping ...

View File

@ -300,8 +300,7 @@ export const EN_US_LANG: any = {
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
},
"REPOSITORY": {
"COPY_ID": "Copy ID",
"COPY_PARENT_ID": "Copy Parent ID",
"COPY_DIGEST_ID": "Copy Digest ID",
"DELETE": "Delete",
"NAME": "Name",
"TAGS_COUNT": "Tags",

View File

@ -300,8 +300,7 @@ export const ZH_CN_LANG: any = {
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
},
"REPOSITORY": {
"COPY_ID": "复制ID",
"COPY_PARENT_ID": "复制父级ID",
"COPY_DIGEST_ID": "复制摘要ID",
"DELETE": "删除",
"NAME": "名称",
"TAGS_COUNT": "标签数",

View File

@ -4,13 +4,14 @@ export const TAG_TEMPLATE = `
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
<div class="row col-md-12">
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea>
<textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>
<h2 *ngIf="!isEmbeded" class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbeded">
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
@ -23,8 +24,7 @@ export const TAG_TEMPLATE = `
<clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.name}}</clr-dg-cell>

View File

@ -57,7 +57,7 @@ export class TagComponent implements OnInit {
showTagManifestOpened: boolean;
manifestInfoTitle: string;
tagID: string;
digestId: string;
staticBackdrop: boolean = true;
closable: boolean = false;
@ -76,24 +76,23 @@ export class TagComponent implements OnInit {
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag: Tag = message.data;
if (tag) {
if (tag.signature) {
return;
} else {
let tagName = tag.name;
toPromise<number>(this.tagService
.deleteTag(this.repoName, tagName))
.then(
response => {
this.retrieve();
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
.subscribe(res=>this.errorHandler.info(res));
}).catch(error => this.errorHandler.error(error));
}
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag: Tag = message.data;
if (tag) {
if (tag.signature) {
return;
} else {
toPromise<number>(this.tagService
.deleteTag(this.repoName, tag.name))
.then(
response => {
this.retrieve();
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
.subscribe(res=>this.errorHandler.info(res));
}).catch(error => this.errorHandler.error(error));
}
}
}
}
@ -163,15 +162,10 @@ export class TagComponent implements OnInit {
}
}
showTagID(type: string, tag: Tag) {
showDigestId(tag: Tag) {
if(tag) {
if(type === 'tag') {
this.manifestInfoTitle = 'REPOSITORY.COPY_ID';
this.tagID = tag.digest;
} else if(type === 'parent') {
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
this.tagID = tag.digest;
}
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.digestId = tag.digest;
this.showTagManifestOpened = true;
}
}

View File

@ -16,7 +16,6 @@ import { Http, URLSearchParams, Response } from '@angular/http';
import { Repository } from './repository';
import { Tag } from './tag';
import { VerifiedSignature } from './verified-signature';
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/of';
@ -46,35 +45,6 @@ export class RepositoryService {
.catch(error=>Observable.throw(error));
}
listNotarySignatures(repoName: string): Observable<VerifiedSignature[]> {
return this.http
.get(`/api/repositories/${repoName}/signatures`)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
listTagsWithVerifiedSignatures(repoName: string): Observable<Tag[]> {
return this.listTags(repoName)
.map(res=>res)
.flatMap(tags=>{
return this.listNotarySignatures(repoName).map(signatures=>{
tags.forEach(t=>{
for(let i = 0; i < signatures.length; i++) {
if(signatures[i].tag === t.tag) {
t.signed = 1;
break;
}
}
});
return tags;
})
.catch(error=>{
return Observable.of(tags);
})
})
.catch(error=>Observable.throw(error));
}
deleteRepository(repoName: string): Observable<any> {
return this.http
.delete(`/api/repositories/${repoName}`)

View File

@ -6,7 +6,7 @@
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
<div class="row col-md-12">
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea>
<textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
</div>
</div>
<div class="modal-footer">
@ -24,25 +24,20 @@
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-row *clrDgItems="let t of tags">
<clr-dg-action-overflow>
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed">
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngSwitchCase="0" style="color: #C92100;"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="help" style="color: #565656;" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary">
<clr-icon *ngIf="t.signature" shape="check" style="color: #1D5100;"></clr-icon>
<clr-icon *ngIf="!t.signature" shape="close" style="color: #C92100;"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</clr-dg-cell>
</clr-dg-row>

View File

@ -24,7 +24,6 @@ import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmati
import { Subscription } from 'rxjs/Subscription';
import { Tag } from '../tag';
import { TagView } from '../tag-view';
import { AppConfigService } from '../../app-config.service';
@ -45,7 +44,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
hasProjectAdminRole: boolean = false;
tags: TagView[];
tags: Tag[];
registryUrl: string;
withNotary: boolean;
@ -53,7 +52,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
showTagManifestOpened: boolean;
manifestInfoTitle: string;
tagID: string;
digestId: string;
staticBackdrop: boolean = true;
closable: boolean = false;
@ -79,9 +78,8 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
if (tag.signed) {
return;
} else {
let tagName = tag.tag;
this.repositoryService
.deleteRepoByTag(this.repoName, tagName)
.deleteRepoByTag(this.repoName, tag.name)
.subscribe(
response => {
this.retrieve();
@ -103,10 +101,11 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
}
this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo'];
this.tags = [];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.withNotary = this.appConfigService.getConfig().with_notary;
this.retrieve();
}
ngOnDestroy() {
@ -120,63 +119,25 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.repositoryService
.listTags(this.repoName)
.subscribe(
items => this.listTags(items),
tags => this.tags = tags,
error => this.messageHandlerService.handleError(error));
if(this.withNotary) {
this.repositoryService
.listNotarySignatures(this.repoName)
.subscribe(
signatures => {
this.tags.forEach((t, n)=>{
let signed = false;
for(let i = 0; i < signatures.length; i++) {
if (signatures[i].tag === t.tag) {
signed = true;
break;
}
}
this.tags[n].signed = (signed) ? 1 : 0;
this.ref.markForCheck();
});
},
error => console.error('Cannot determine the signature of this tag.'));
}
}
listTags(tags: Tag[]): void {
tags.forEach(t => {
let tag = new TagView();
tag.tag = t.tag;
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
tag.signed = t.signed;
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
tag.id = data['id'];
tag.parent = data['parent'];
this.tags.push(tag);
});
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
deleteTag(tag: TagView) {
deleteTag(tag: Tag) {
if (tag) {
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
if (tag.signed) {
if (tag.signature) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
buttons = ConfirmationButtons.CLOSE;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
} else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tag.tag;
content = tag.name;
}
let message = new ConfirmationMessage(
titleKey,
@ -189,15 +150,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
}
}
showTagID(type: string, tag: TagView) {
showDigestId(tag: Tag) {
if(tag) {
if(type === 'tag') {
this.manifestInfoTitle = 'REPOSITORY.COPY_ID';
this.tagID = tag.id;
} else if(type === 'parent') {
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
this.tagID = tag.parent;
}
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.digestId = tag.digest;
this.showTagManifestOpened = true;
}
}

View File

@ -1,25 +0,0 @@
// Copyright (c) 2017 VMware, Inc. 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.
export class TagView {
tag: string;
pullCommand: string;
signed: number = -1;
author: string;
created: Date;
dockerVersion: string;
architecture: string;
os: string;
id: string;
parent: string;
}

View File

@ -11,30 +11,13 @@
// 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.
/*
{
"tag": "latest",
"manifest": {
"schemaVersion": 1,
"name": "library/photon",
"tag": "latest",
"architecture": "amd64",
"history": []
},
*/
export class Tag {
tag: string;
manifest: {
schemaVersion: number;
name: string;
tag: string;
architecture: string;
history: [
{
v1Compatibility: string;
}
];
};
signed: number;
digest: string;
name: string;
architecture: string;
os: string;
docker_version: string;
author: string;
created: Date;
signature?: {[key: string]: any | any[]}
}

View File

@ -1,30 +0,0 @@
// Copyright (c) 2017 VMware, Inc. 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.
/*
[
{
"tag": "2.0",
"hashes": {
"sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
}
}
]
*/
export class VerifiedSignature {
tag: string;
hashes: {
sha256: string;
}
}

View File

@ -300,8 +300,7 @@
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
},
"REPOSITORY": {
"COPY_ID": "Copy ID",
"COPY_PARENT_ID": "Copy Parent ID",
"COPY_DIGEST_ID": "Copy Digest ID",
"DELETE": "Delete",
"NAME": "Name",
"TAGS_COUNT": "Tags",

View File

@ -300,8 +300,7 @@
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
},
"REPOSITORY": {
"COPY_ID": "复制ID",
"COPY_PARENT_ID": "复制父级ID",
"COPY_DIGEST_ID": "复制摘要ID",
"DELETE": "删除",
"NAME": "名称",
"TAGS_COUNT": "标签数",