merge code from master and fix conflicts

Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
Steven Zou 2019-07-24 17:27:37 +08:00
commit c44747fd3c
37 changed files with 408 additions and 194 deletions

View File

@ -3478,6 +3478,37 @@ paths:
description: The robot account is not found.
'500':
description: Unexpected internal errors.
'/system/oidc/ping':
post:
summary: Test the OIDC endpoint.
description: Test the OIDC endpoint, the setting of the endpoint is provided in the request. This API can only
be called by system admin.
tags:
- Products
- System
parameters:
- name: endpoint
in: body
description: Request body for OIDC endpoint to be tested.
required: true
schema:
type: object
properties:
url:
type: string
description: The URL of OIDC endpoint to be tested.
verify_cert:
type: boolean
description: Whether the certificate should be verified
responses:
'200':
description: Ping succeeded. The OIDC endpoint is valid.
'400':
description: The ping failed
'401':
description: User need to log in first.
'403':
description: User does not have permission to call this API
'/system/CVEWhitelist':
get:
summary: Get the system level whitelist of CVE.

View File

@ -1,16 +1,16 @@
package chartserver
import (
"errors"
"fmt"
commonhttp "github.com/goharbor/harbor/src/common/http"
hlog "github.com/goharbor/harbor/src/common/utils/log"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/pkg/errors"
)
const (
@ -49,11 +49,13 @@ func NewChartClient(credential *Credential) *ChartClient { // Create http client
func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
response, err := cc.sendRequest(addr, http.MethodGet, nil)
if err != nil {
err = errors.Wrap(err, "get content failed")
return nil, err
}
content, err := ioutil.ReadAll(response.Body)
if err != nil {
err = errors.Wrap(err, "Read response body error")
return nil, err
}
defer response.Body.Close()
@ -61,6 +63,7 @@ func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
if response.StatusCode != http.StatusOK {
text, err := extractError(content)
if err != nil {
err = errors.Wrap(err, "Extract content error failed")
return nil, err
}
return nil, &commonhttp.Error{
@ -106,7 +109,8 @@ func (cc *ChartClient) sendRequest(addr string, method string, body io.Reader) (
fullURI, err := url.Parse(addr)
if err != nil {
return nil, fmt.Errorf("invalid url: %s", err.Error())
err = errors.Wrap(err, "Invalid url")
return nil, err
}
request, err := http.NewRequest(method, addr, body)
@ -121,7 +125,7 @@ func (cc *ChartClient) sendRequest(addr string, method string, body io.Reader) (
response, err := cc.httpClient.Do(request)
if err != nil {
hlog.Errorf("%s '%s' failed with error: %s", method, fullURI.Path, err)
err = errors.Wrap(err, fmt.Sprintf("send request %s %s failed", method, fullURI.Path))
return nil, err
}

View File

@ -2,19 +2,17 @@ package chartserver
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"github.com/ghodss/yaml"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/replication"
rep_event "github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model"
"github.com/pkg/errors"
helm_repo "k8s.io/helm/pkg/repo"
"os"
"github.com/goharbor/harbor/src/common/utils/log"
)
// ListCharts gets the chart list under the namespace

View File

@ -20,12 +20,11 @@ import (
"net/http"
"strconv"
"github.com/astaxie/beego"
"github.com/astaxie/beego/validation"
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/utils/log"
"errors"
"github.com/astaxie/beego"
"github.com/pkg/errors"
)
const (

View File

@ -144,11 +144,7 @@ func UpdateUserGroupName(id int, groupName string) error {
return err
}
// OnBoardUserGroup will check if a usergroup exists in usergroup table, if not insert the usergroup and
// put the id in the pointer of usergroup model, if it does exist, return the usergroup's profile.
// This is used for ldap and uaa authentication, such the usergroup can have an ID in Harbor.
// the keyAttribute and combinedKeyAttribute are key columns used to check duplicate usergroup in harbor
func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttributes ...string) error {
func onBoardCommonUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttributes ...string) error {
g.LdapGroupDN = utils.TrimLower(g.LdapGroupDN)
o := dao.GetOrmer()
@ -172,3 +168,12 @@ func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttri
return nil
}
// OnBoardUserGroup will check if a usergroup exists in usergroup table, if not insert the usergroup and
// put the id in the pointer of usergroup model, if it does exist, return the usergroup's profile.
func OnBoardUserGroup(g *models.UserGroup) error {
if g.GroupType == common.LDAPGroupType {
return onBoardCommonUserGroup(g, "LdapGroupDN", "GroupType")
}
return onBoardCommonUserGroup(g, "GroupName", "GroupType")
}

View File

@ -256,7 +256,7 @@ func TestOnBoardUserGroup(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := OnBoardUserGroup(tt.args.g, "LdapGroupDN", "GroupType"); (err != nil) != tt.wantErr {
if err := OnBoardUserGroup(tt.args.g); (err != nil) != tt.wantErr {
t.Errorf("OnBoardUserGroup() error = %v, wantErr %v", err, tt.wantErr)
}
})

View File

@ -206,3 +206,19 @@ func RefreshToken(ctx context.Context, token *Token) (*Token, error) {
}
return &Token{Token: *t, IDToken: it}, nil
}
// Conn wraps connection info of an OIDC endpoint
type Conn struct {
URL string `json:"url"`
VerifyCert bool `json:"verify_cert"`
}
// TestEndpoint tests whether the endpoint is a valid OIDC endpoint.
// The nil return value indicates the success of the test
func TestEndpoint(conn Conn) error {
// gooidc will try to call the discovery api when creating the provider and that's all we need to check
ctx := clientCtx(context.Background(), conn.VerifyCert)
_, err := gooidc.NewProvider(ctx, conn.URL)
return err
}

View File

@ -97,3 +97,16 @@ func TestAuthCodeURL(t *testing.T) {
assert.Equal(t, "offline", q.Get("access_type"))
assert.False(t, strings.Contains(q.Get("scope"), "offline_access"))
}
func TestTestEndpoint(t *testing.T) {
c1 := Conn{
URL: googleEndpoint,
VerifyCert: true,
}
c2 := Conn{
URL: "https://www.baidu.com",
VerifyCert: false,
}
assert.Nil(t, TestEndpoint(c1))
assert.NotNil(t, TestEndpoint(c2))
}

View File

@ -16,8 +16,10 @@ package api
import (
"errors"
"github.com/goharbor/harbor/src/pkg/retention"
"github.com/goharbor/harbor/src/pkg/scheduler"
"net/http"
"github.com/ghodss/yaml"
@ -33,10 +35,6 @@ import (
const (
yamlFileContentType = "application/x-yaml"
// ReplicationJobType ...
ReplicationJobType = "replication"
// ScanJobType ...
ScanJobType = "scan"
)
// the managers/controllers used globally
@ -96,7 +94,7 @@ func (b *BaseController) WriteYamlData(object interface{}) {
w := b.Ctx.ResponseWriter
w.Header().Set("Content-Type", yamlFileContentType)
w.WriteHeader(http.StatusOK)
w.Write(yData)
_, _ = w.Write(yData)
}
// Init related objects/configurations for the API controllers

View File

@ -145,6 +145,7 @@ func init() {
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/CVEWhitelist", &SysCVEWhitelistAPI{}, "get:Get;put:Put")
beego.Router("/api/system/oidc/ping", &OIDCAPI{}, "post:Ping")
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")

56
src/core/api/oidc.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright Project Harbor Authors
//
// 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.
package api
import (
"errors"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/oidc"
)
// OIDCAPI handles the requests to /api/system/oidc/xxx
type OIDCAPI struct {
BaseController
}
// Prepare validates the request initially
func (oa *OIDCAPI) Prepare() {
oa.BaseController.Prepare()
if !oa.SecurityCtx.IsAuthenticated() {
oa.SendUnAuthorizedError(errors.New("unauthorized"))
return
}
if !oa.SecurityCtx.IsSysAdmin() {
msg := "only system admin has permission to access this API"
log.Errorf(msg)
oa.SendForbiddenError(errors.New(msg))
return
}
}
// Ping will handles the request to test connection to OIDC endpoint
func (oa *OIDCAPI) Ping() {
var c oidc.Conn
if err := oa.DecodeJSONReq(&c); err != nil {
log.Error("Failed to decode JSON request.")
oa.SendBadRequestError(err)
return
}
if err := oidc.TestEndpoint(c); err != nil {
log.Errorf("Failed to verify connection: %+v, err: %v", c, err)
oa.SendBadRequestError(err)
return
}
}

69
src/core/api/oidc_test.go Normal file
View File

@ -0,0 +1,69 @@
// Copyright Project Harbor Authors
//
// 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.
package api
import (
"github.com/goharbor/harbor/src/common/utils/oidc"
"net/http"
"testing"
)
func TestOIDCAPI_Ping(t *testing.T) {
url := "/api/system/oidc/ping"
cases := []*codeCheckingCase{
{ // 401
request: &testingRequest{
method: http.MethodPost,
bodyJSON: oidc.Conn{},
url: url,
},
code: http.StatusUnauthorized,
},
{ // 403
request: &testingRequest{
method: http.MethodPost,
bodyJSON: oidc.Conn{},
url: url,
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
{ // 400
request: &testingRequest{
method: http.MethodPost,
bodyJSON: oidc.Conn{
URL: "https://www.baidu.com",
VerifyCert: true,
},
url: url,
credential: sysAdmin,
},
code: http.StatusBadRequest,
},
{ // 200
request: &testingRequest{
method: http.MethodPost,
bodyJSON: oidc.Conn{
URL: "https://accounts.google.com",
VerifyCert: true,
},
url: url,
credential: sysAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -18,6 +18,8 @@ import (
"fmt"
"net/http"
"regexp"
"strconv"
"time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
@ -27,10 +29,7 @@ import (
errutil "github.com/goharbor/harbor/src/common/utils/error"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"errors"
"strconv"
"time"
"github.com/pkg/errors"
)
type deletableResp struct {
@ -232,7 +231,10 @@ func (p *ProjectAPI) Get() {
return
}
p.populateProperties(p.project)
err := p.populateProperties(p.project)
if err != nil {
log.Errorf("populate project poroperties failed with : %+v", err)
}
p.Data["json"] = p.project
p.ServeJSON()
@ -402,15 +404,17 @@ func (p *ProjectAPI) List() {
}
for _, project := range result.Projects {
p.populateProperties(project)
err = p.populateProperties(project)
if err != nil {
log.Errorf("populate project properties failed %v", err)
}
}
p.SetPaginationHeader(result.Total, page, size)
p.Data["json"] = result.Projects
p.ServeJSON()
}
func (p *ProjectAPI) populateProperties(project *models.Project) {
func (p *ProjectAPI) populateProperties(project *models.Project) error {
if p.SecurityCtx.IsAuthenticated() {
roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
if len(roles) != 0 {
@ -427,9 +431,8 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
ProjectIDs: []int64{project.ProjectID},
})
if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", project.ProjectID, err)
p.SendInternalServerError(errors.New(""))
return
err = errors.Wrap(err, fmt.Sprintf("get repo count of project %d failed", project.ProjectID))
return err
}
project.RepoCount = total
@ -438,13 +441,13 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
if config.WithChartMuseum() {
count, err := chartController.GetCountOfCharts([]string{project.Name})
if err != nil {
log.Errorf("Failed to get total of charts under project %s: %v", project.Name, err)
p.SendInternalServerError(errors.New(""))
return
err = errors.Wrap(err, fmt.Sprintf("get chart count of project %d failed", project.ProjectID))
return err
}
project.ChartCount = count
}
return nil
}
// Put ...

View File

@ -17,6 +17,7 @@ package authproxy
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@ -190,12 +191,14 @@ func (a *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) {
// OnBoardGroup create user group entity in Harbor DB, altGroupName is not used.
func (a *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
// if group name provided, on board the user group
userGroup := &models.UserGroup{GroupName: u.GroupName, GroupType: common.HTTPGroupType}
err := group.OnBoardUserGroup(u, "GroupName", "GroupType")
if len(u.GroupName) == 0 {
return errors.New("Should provide a group name")
}
u.GroupType = common.HTTPGroupType
err := group.OnBoardUserGroup(u)
if err != nil {
return err
}
u.ID = userGroup.ID
return nil
}

View File

@ -43,6 +43,7 @@ func TestMain(m *testing.M) {
}
mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"})
defer mockSvr.Close()
defer dao.ExecuteBatchSQL([]string{"delete from user_group where group_name='OnBoardTest'"})
a = &Auth{
Endpoint: mockSvr.URL + "/test/login",
TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview",
@ -50,10 +51,17 @@ func TestMain(m *testing.M) {
// So it won't require mocking the cfgManager
settingTimeStamp: time.Now(),
}
cfgMap := cut.GetUnitTestConfig()
conf := map[string]interface{}{
common.HTTPAuthProxyEndpoint: a.Endpoint,
common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint,
common.HTTPAuthProxyVerifyCert: !a.SkipCertVerify,
common.PostGreSQLSSLMode: cfgMap[common.PostGreSQLSSLMode],
common.PostGreSQLUsername: cfgMap[common.PostGreSQLUsername],
common.PostGreSQLPort: cfgMap[common.PostGreSQLPort],
common.PostGreSQLHOST: cfgMap[common.PostGreSQLHOST],
common.PostGreSQLPassword: cfgMap[common.PostGreSQLPassword],
common.PostGreSQLDatabase: cfgMap[common.PostGreSQLDatabase],
}
config.InitWithSettings(conf)
@ -174,3 +182,19 @@ func TestAuth_PostAuthenticate(t *testing.T) {
}
}
func TestAuth_OnBoardGroup(t *testing.T) {
input := &models.UserGroup{
GroupName: "OnBoardTest",
GroupType: common.HTTPGroupType,
}
a.OnBoardGroup(input, "")
assert.True(t, input.ID > 0, "The OnBoardGroup should have a valid group ID")
emptyGroup := &models.UserGroup{}
err := a.OnBoardGroup(emptyGroup, "")
if err == nil {
t.Fatal("Empty user group should failed to OnBoard")
}
}

View File

@ -214,7 +214,7 @@ func (l *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
if len(userGroupList) > 0 {
return auth.ErrDuplicateLDAPGroup
}
return group.OnBoardUserGroup(u, "LdapGroupDN", "GroupType")
return group.OnBoardUserGroup(u)
}
// PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser

View File

@ -98,6 +98,7 @@ func initRouters() {
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/CVEWhitelist", &api.SysCVEWhitelistAPI{}, "get:Get;put:Put")
beego.Router("/api/system/oidc/ping", &api.OIDCAPI{}, "post:Ping")
beego.Router("/api/logs", &api.LogAPI{})

View File

@ -21,18 +21,10 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"context"
"github.com/goharbor/harbor/src/jobservice/common/utils"
)
const (
proxyEnvHTTP = "http_proxy"
proxyEnvHTTPS = "https_proxy"
)
// Client for handling the hook events
@ -60,19 +52,7 @@ func NewClient(ctx context.Context) Client {
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// Get the http/https proxies
proxyAddr, ok := os.LookupEnv(proxyEnvHTTP)
if !ok {
proxyAddr, ok = os.LookupEnv(proxyEnvHTTPS)
}
if ok && !utils.IsEmptyStr(proxyAddr) {
proxyURL, err := url.Parse(proxyAddr)
if err == nil {
transport.Proxy = http.ProxyURL(proxyURL)
}
Proxy: http.ProxyFromEnvironment,
}
client := &http.Client{

View File

@ -99,6 +99,7 @@
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
</div>
<div class="add-modal" *ngIf="showAddModal">
<clr-icon (click)="showAddModal=false" class="float-lg-right margin-top-4" shape="window-close"></clr-icon>
<div>
<clr-textarea-container>
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
@ -115,7 +116,8 @@
<ul class="whitelist-window">
<li *ngIf="systemWhitelist?.items?.length<1"
class="none">{{'CVE_WHITELIST.NONE'|translate}}</li>
<li *ngFor="let item of systemWhitelist?.items;let i = index;">{{item.cve_id}}
<li *ngFor="let item of systemWhitelist?.items;let i = index;">
<span class="hand" (click)="goToDetail(item.cve_id)">{{item.cve_id}}</span>
<clr-icon (click)="deleteItem(i)" class="float-lg-right margin-top-4"
shape="times-circle"></clr-icon>
</li>

View File

@ -31,6 +31,7 @@
li {
height: 24px;
line-height: 24px;
list-style-type: none;
}
}
@ -72,4 +73,8 @@
button {
float: right;
}
}
.hand{
cursor: pointer;
margin: 0;
}

View File

@ -28,6 +28,8 @@ const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
const ONE_HOUR_MINUTES: number = 60;
const ONE_DAY_MINUTES: number = 24 * ONE_HOUR_MINUTES;
const ONE_THOUSAND: number = 1000;
const CVE_DETAIL_PRE_URL = `https://nvd.nist.gov/vuln/detail/`;
const TARGET_BLANK = "_blank";
@Component({
selector: 'system-settings',
@ -380,4 +382,8 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
}
return false;
}
goToDetail(cveId) {
window.open(CVE_DETAIL_PRE_URL + `${cveId}`, TARGET_BLANK);
}
}

View File

@ -11,17 +11,17 @@
</clr-tooltip>
<clr-dropdown *ngIf="isClairDBFullyReady && showScanningNamespaces" class="clr-dropdown-override">
<button class="btn btn-link btn-font" clrDropdownToggle>
{{ updatedTimestamp | date:'MM/dd/y HH:mm:ss' }} AM
{{ updatedTimestamp | date:'short' }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu [clrPosition]="'bottom-right'" class="dropdown-namespace">
<div *ngFor="let nt of namespaceTimestamps" class="namespace">
<span class="label label-info">{{nt.namespace}}</span>
<span>{{ convertToLocalTime(nt.last_update) | date:'MM/dd/y HH:mm:ss'}} AM</span>
<span>{{ convertToLocalTime(nt.last_update) | date:'short'}} </span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<span *ngIf="isClairDBFullyReady && !showScanningNamespaces">{{ updatedTimestamp | date:'MM/dd/y HH:mm:ss' }} AM</span>
<span *ngIf="isClairDBFullyReady && !showScanningNamespaces">{{ updatedTimestamp | date:'short' }} </span>
</div>
<div class="button-group">
<cron-selection #CronScheduleComponent [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent' [originCron]='originCron' (inputvalue)="scanAll($event)"></cron-selection>

View File

@ -109,6 +109,7 @@
class="btn btn-link ml-1">{{'CVE_WHITELIST.ADD_SYSTEM'|translate}}</button>
</div>
<div class="add-modal" *ngIf="showAddModal && !isUseSystemWhitelist()">
<clr-icon (click)="showAddModal=false" class="float-lg-right margin-top-4" shape="window-close"></clr-icon>
<div>
<clr-textarea-container>
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
@ -124,12 +125,15 @@
<ul class="whitelist-window" *ngIf="isUseSystemWhitelist()">
<li *ngIf="systemWhitelist?.items?.length<1"
class="none">{{'CVE_WHITELIST.NONE'|translate}}</li>
<li *ngFor="let item of systemWhitelist?.items">{{item.cve_id}}</li>
<li *ngFor="let item of systemWhitelist?.items">
<span class="hand" (click)="goToDetail(item.cve_id)">{{item.cve_id}}</span>
</li>
</ul>
<ul class="whitelist-window" *ngIf="!isUseSystemWhitelist()">
<li class="none"
*ngIf="projectWhitelist?.items?.length<1">{{'CVE_WHITELIST.NONE'|translate}}</li>
<li *ngFor="let item of projectWhitelist?.items;let i = index;">{{item.cve_id}}
<li *ngFor="let item of projectWhitelist?.items;let i = index;">
<span class="hand" (click)="goToDetail(item.cve_id)">{{item.cve_id}}</span>
<clr-icon (click)="deleteItem(i)" class="float-lg-right margin-top-4"
shape="times-circle"></clr-icon>
</li>

View File

@ -5,6 +5,9 @@
.select {
width: 120px;
}
.margin-top-4 {
margin-top: 4px;
}
.whitelist-window {
border: 1px solid #ccc;
@ -18,6 +21,7 @@
li {
height: 24px;
line-height: 24px;
list-style-type: none;
}
}
@ -61,3 +65,8 @@
}
}
.hand{
cursor: pointer;
margin: 0;
}

View File

@ -19,6 +19,8 @@ import {USERSTATICPERMISSION} from '../service/permission-static';
const ONE_THOUSAND: number = 1000;
const LOW: string = 'low';
const CVE_DETAIL_PRE_URL = `https://nvd.nist.gov/vuln/detail/`;
const TARGET_BLANK = "_blank";
export class ProjectPolicy {
Public: boolean;
@ -367,4 +369,7 @@ export class ProjectPolicyConfigComponent implements OnInit {
}
return false;
}
goToDetail(cveId) {
window.open(CVE_DETAIL_PRE_URL + `${cveId}`, TARGET_BLANK);
}
}

View File

@ -44,42 +44,46 @@
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
</label>
</div>
<div class="clr-col">
<div class="form-group padding-left-120">
<label>{{'ROBOT_ACCOUNT.PERMISSIONS_IMAGE' | translate}}</label>
<div class="radio-inline">
<input type="radio" name="image-permission"
id="image-permission-pull"
value="pull"
[(ngModel)]="imagePermission">
<label for="image-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
</div>
<div class="radio-inline">
<input type="radio" name="image-permission"
id="image-permission-push-and-pull"
value="push-and-pull"
[(ngModel)]="imagePermission">
<label for="image-permission-push-and-pull">{{'ROBOT_ACCOUNT.PUSH' | translate}}
& {{'ROBOT_ACCOUNT.PULL' | translate}}</label>
</div>
</div>
<div class="form-group padding-left-120">
<label>{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</label>
<div class="checkbox-inline">
<input type="checkbox" id="helm-permission-push"
[checked]="robot.access.isPushChart"
[(ngModel)]="robot.access.isPushChart"
name="helm-permission">
<label for="helm-permission-push">{{'ROBOT_ACCOUNT.PUSH' | translate}}</label>
</div>
<div class="checkbox-inline">
<input type="checkbox" id="helm-permission-pull"
[checked]="robot.access.isPullChart"
[(ngModel)]="robot.access.isPullChart"
name="helm-permission">
<label for="helm-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
</div>
</div>
<div class="clr-col p-0">
<table class="table table-noborder m-0 w-90">
<tr>
<th></th>
<th class="left">{{'ROBOT_ACCOUNT.PUSH' | translate}}</th>
<th class="left">{{'ROBOT_ACCOUNT.PULL' | translate}}</th>
</tr>
<tr>
<td class="left">
<span>{{'ROBOT_ACCOUNT.PERMISSIONS_IMAGE' | translate}}</span>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'ROBOT_ACCOUNT.PULL_IS_MUST' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</td>
<td>
<input type="checkbox" name="image-permission-push"
[(ngModel)]="imagePermissionPush" clrCheckbox>
</td>
<td class="clr-form-control-disabled">
<input disabled type="checkbox" name="image-permission-pull"
[(ngModel)]="imagePermissionPull" clrCheckbox>
</td>
</tr>
<tr>
<td class="left">{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</td>
<td>
<input type="checkbox"
[(ngModel)]="robot.access.isPushChart"
name="helm-permission" clrCheckbox>
</td>
<td>
<input type="checkbox"
[(ngModel)]="robot.access.isPullChart"
name="helm-permission" clrCheckbox>
</td>
</tr>
</table>
</div>
</div>
</section>

View File

@ -42,5 +42,8 @@
}
.padding-left-120{
padding-left: 120px;
padding-left: 126px;
}
.w-90{
width: 90%;
}

View File

@ -38,7 +38,8 @@ export class AddRobotComponent implements OnInit, OnDestroy {
robotNameChecker: Subject<string> = new Subject<string>();
nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
robotForm: NgForm;
imagePermission: string = "push-and-pull";
imagePermissionPush: boolean = true;
imagePermissionPull: boolean = true;
@Input() projectId: number;
@Input() projectName: string;
@Output() create = new EventEmitter<boolean>();
@ -99,6 +100,8 @@ export class AddRobotComponent implements OnInit, OnDestroy {
this.robot.name = "";
this.robot.description = "";
this.addRobotOpened = true;
this.imagePermissionPush = true;
this.imagePermissionPull = true;
this.isRobotNameValid = true;
this.robot = new Robot();
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
@ -118,12 +121,12 @@ export class AddRobotComponent implements OnInit, OnDestroy {
return;
}
// set value to robot.access.isPullImage and robot.access.isPushOrPullImage when submit
if ( this.imagePermission === 'pull' ) {
this.robot.access.isPullImage = true;
this.robot.access.isPushOrPullImage = false;
} else {
if ( this.imagePermissionPush && this.imagePermissionPull) {
this.robot.access.isPullImage = false;
this.robot.access.isPushOrPullImage = true;
} else {
this.robot.access.isPullImage = true;
this.robot.access.isPushOrPullImage = false;
}
this.isSubmitOnGoing = true;
this.robotService

View File

@ -322,7 +322,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?"
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
},
"GROUP": {
"GROUP": "Group",

View File

@ -323,7 +323,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?"
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
},
"GROUP": {
"GROUP": "Group",

View File

@ -315,7 +315,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ",
"DELETION_SUMMARY": "Voulez-vous supprimer la règle {{param}}?"
"DELETION_SUMMARY": "Voulez-vous supprimer la règle {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
},
"GROUP": {
"Group": "Group",

View File

@ -320,7 +320,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirmar a remoção do robô Contas",
"DELETION_SUMMARY": "Você quer remover a regra {{param}}?"
"DELETION_SUMMARY": "Você quer remover a regra {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
},
"GROUP": {
"GROUP": "Grupo",

View File

@ -321,7 +321,8 @@
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功。",
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
"DELETION_TITLE": "删除账户确认",
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?"
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?",
"PULL_IS_MUST" : "拉取权限默认选中且不可修改。"
},
"GROUP": {
"GROUP": "组",

View File

@ -1,8 +1,6 @@
package huawei
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
@ -10,7 +8,10 @@ import (
"regexp"
"strings"
common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/registry/auth"
adp "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/adapter/native"
"github.com/goharbor/harbor/src/replication/model"
@ -30,6 +31,7 @@ func init() {
type adapter struct {
*native.Adapter
registry *model.Registry
client *common_http.Client
}
// Info gets info about Huawei SWR
@ -56,18 +58,8 @@ func (a *adapter) ListNamespaces(query *model.NamespaceQuery) ([]*model.Namespac
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return namespaces, err
}
@ -120,8 +112,11 @@ func (a *adapter) ConvertResourceMetadata(resourceMetadata *model.ResourceMetada
func (a *adapter) PrepareForPush(resources []*model.Resource) error {
namespaces := map[string]struct{}{}
for _, resource := range resources {
var namespace string
paths := strings.Split(resource.Metadata.Repository.Name, "/")
namespace := paths[0]
if len(paths) > 0 {
namespace = paths[0]
}
ns, err := a.GetNamespace(namespace)
if err != nil {
return err
@ -133,9 +128,7 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
}
url := fmt.Sprintf("%s/dockyard/v2/namespaces", a.registry.URL)
client := &http.Client{
Transport: util.GetHTTPTransport(a.registry.Insecure),
}
for namespace := range namespaces {
namespacebyte, err := json.Marshal(struct {
Namespace string `json:"namespace"`
@ -152,10 +145,8 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return err
}
@ -185,20 +176,8 @@ func (a *adapter) GetNamespace(namespaceStr string) (*model.Namespace, error) {
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
var client *http.Client
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
} else {
client = &http.Client{}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return namespace, err
}
@ -237,9 +216,30 @@ func AdapterFactory(registry *model.Registry) (adp.Adapter, error) {
if err != nil {
return nil, err
}
var (
modifiers = []modifier.Modifier{
&auth.UserAgentModifier{
UserAgent: adp.UserAgentReplication,
}}
authorizer modifier.Modifier
)
if registry.Credential != nil {
authorizer = auth.NewBasicAuthCredential(
registry.Credential.AccessKey,
registry.Credential.AccessSecret)
modifiers = append(modifiers, authorizer)
}
return &adapter{
registry: registry,
Adapter: dockerRegistryAdapter,
registry: registry,
client: common_http.NewClient(
&http.Client{
Transport: util.GetHTTPTransport(registry.Insecure),
},
modifiers...,
),
}, nil
}

View File

@ -1,8 +1,6 @@
package huawei
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
@ -25,18 +23,8 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return resources, err
}
@ -82,15 +70,7 @@ func (a *adapter) ManifestExist(repository, reference string) (exist bool, diges
r.Header.Add("content-type", "application/json; charset=utf-8")
r.Header.Add("Authorization", "Bearer "+token.Token)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return exist, digest, err
}
@ -133,15 +113,7 @@ func (a *adapter) DeleteManifest(repository, reference string) error {
r.Header.Add("content-type", "application/json; charset=utf-8")
r.Header.Add("Authorization", "Bearer "+token.Token)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return err
}
@ -220,18 +192,8 @@ func getJwtToken(a *adapter, repository string) (token jwtToken, err error) {
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return token, err
}

View File

@ -1,6 +1,7 @@
package huawei
import (
"os"
"strings"
"testing"
@ -20,7 +21,11 @@ func init() {
Insecure: false,
Status: "",
}
HWAdapter.registry = hwRegistry
adp, err := AdapterFactory(hwRegistry)
if err != nil {
os.Exit(1)
}
HWAdapter = *adp.(*adapter)
}
func TestAdapter_FetchImages(t *testing.T) {

View File

@ -25,6 +25,7 @@ sleep 2
sudo -E env "PATH=$PATH" make go_check
sudo ./tests/hostcfg.sh
sudo ./tests/generateCerts.sh
sudo make -f make/photon/Makefile _build_db _build_registry _build_prepare -e VERSIONTAG=dev -e CLAIRDBVERSION=dev -e REGISTRYVERSION=${REG_VERSION}
sudo MAKEPATH=$(pwd)/make ./make/prepare
sudo mkdir -p "/data/redis"
sudo mkdir -p /etc/core/ca/ && sudo mv ./tests/ca.crt /etc/core/ca/
@ -32,7 +33,6 @@ sudo mkdir -p /harbor && sudo mv ./VERSION /harbor/UIVERSION
sudo ./tests/testprepare.sh
cd tests && sudo ./ldapprepare.sh && sudo ./admiral.sh && cd ..
sudo make -f make/photon/Makefile _build_db _build_registry -e VERSIONTAG=dev -e CLAIRDBVERSION=dev -e REGISTRYVERSION=${REG_VERSION}
sudo sed -i 's/__reg_version__/${REG_VERSION}-dev/g' ./make/docker-compose.test.yml
sudo sed -i 's/__version__/dev/g' ./make/docker-compose.test.yml
sudo mkdir -p ./make/common/config/registry/ && sudo mv ./tests/reg_config.yml ./make/common/config/registry/config.yml