Merge branch 'master' of https://github.com/goharbor/harbor into project-quota-dev

This commit is contained in:
wang yan 2019-07-22 15:46:09 +08:00
commit 2292954a31
29 changed files with 329 additions and 109 deletions

View File

@ -3478,6 +3478,37 @@ paths:
description: The robot account is not found. description: The robot account is not found.
'500': '500':
description: Unexpected internal errors. 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': '/system/CVEWhitelist':
get: get:
summary: Get the system level whitelist of CVE. summary: Get the system level whitelist of CVE.

View File

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

View File

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

View File

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

View File

@ -144,11 +144,7 @@ func UpdateUserGroupName(id int, groupName string) error {
return err return err
} }
// OnBoardUserGroup will check if a usergroup exists in usergroup table, if not insert the usergroup and func onBoardCommonUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttributes ...string) error {
// 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 {
g.LdapGroupDN = utils.TrimLower(g.LdapGroupDN) g.LdapGroupDN = utils.TrimLower(g.LdapGroupDN)
o := dao.GetOrmer() o := dao.GetOrmer()
@ -172,3 +168,12 @@ func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttri
return nil 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) 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 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.Equal(t, "offline", q.Get("access_type"))
assert.False(t, strings.Contains(q.Get("scope"), "offline_access")) 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

@ -15,9 +15,9 @@
package api package api
import ( import (
"errors"
"net/http" "net/http"
"errors"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/goharbor/harbor/src/common/api" "github.com/goharbor/harbor/src/common/api"
"github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/security"

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/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/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/CVEWhitelist", &SysCVEWhitelistAPI{}, "get:Get;put:Put") 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/", &RobotAPI{}, "post:Post;get:List")
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete") 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" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"time"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
@ -27,10 +29,7 @@ import (
errutil "github.com/goharbor/harbor/src/common/utils/error" errutil "github.com/goharbor/harbor/src/common/utils/error"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
"github.com/pkg/errors"
"errors"
"strconv"
"time"
) )
type deletableResp struct { type deletableResp struct {
@ -232,7 +231,10 @@ func (p *ProjectAPI) Get() {
return 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.Data["json"] = p.project
p.ServeJSON() p.ServeJSON()
@ -402,15 +404,17 @@ func (p *ProjectAPI) List() {
} }
for _, project := range result.Projects { 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.SetPaginationHeader(result.Total, page, size)
p.Data["json"] = result.Projects p.Data["json"] = result.Projects
p.ServeJSON() p.ServeJSON()
} }
func (p *ProjectAPI) populateProperties(project *models.Project) { func (p *ProjectAPI) populateProperties(project *models.Project) error {
if p.SecurityCtx.IsAuthenticated() { if p.SecurityCtx.IsAuthenticated() {
roles := p.SecurityCtx.GetProjectRoles(project.ProjectID) roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
if len(roles) != 0 { if len(roles) != 0 {
@ -427,9 +431,8 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
ProjectIDs: []int64{project.ProjectID}, ProjectIDs: []int64{project.ProjectID},
}) })
if err != nil { if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", project.ProjectID, err) err = errors.Wrap(err, fmt.Sprintf("get repo count of project %d failed", project.ProjectID))
p.SendInternalServerError(errors.New("")) return err
return
} }
project.RepoCount = total project.RepoCount = total
@ -438,13 +441,13 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
if config.WithChartMuseum() { if config.WithChartMuseum() {
count, err := chartController.GetCountOfCharts([]string{project.Name}) count, err := chartController.GetCountOfCharts([]string{project.Name})
if err != nil { if err != nil {
log.Errorf("Failed to get total of charts under project %s: %v", project.Name, err) err = errors.Wrap(err, fmt.Sprintf("get chart count of project %d failed", project.ProjectID))
p.SendInternalServerError(errors.New("")) return err
return
} }
project.ChartCount = count project.ChartCount = count
} }
return nil
} }
// Put ... // Put ...

View File

@ -17,6 +17,7 @@ package authproxy
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "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. // OnBoardGroup create user group entity in Harbor DB, altGroupName is not used.
func (a *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error { func (a *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
// if group name provided, on board the user group // if group name provided, on board the user group
userGroup := &models.UserGroup{GroupName: u.GroupName, GroupType: common.HTTPGroupType} if len(u.GroupName) == 0 {
err := group.OnBoardUserGroup(u, "GroupName", "GroupType") return errors.New("Should provide a group name")
}
u.GroupType = common.HTTPGroupType
err := group.OnBoardUserGroup(u)
if err != nil { if err != nil {
return err return err
} }
u.ID = userGroup.ID
return nil 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"}) mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"})
defer mockSvr.Close() defer mockSvr.Close()
defer dao.ExecuteBatchSQL([]string{"delete from user_group where group_name='OnBoardTest'"})
a = &Auth{ a = &Auth{
Endpoint: mockSvr.URL + "/test/login", Endpoint: mockSvr.URL + "/test/login",
TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview", TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview",
@ -50,10 +51,17 @@ func TestMain(m *testing.M) {
// So it won't require mocking the cfgManager // So it won't require mocking the cfgManager
settingTimeStamp: time.Now(), settingTimeStamp: time.Now(),
} }
cfgMap := cut.GetUnitTestConfig()
conf := map[string]interface{}{ conf := map[string]interface{}{
common.HTTPAuthProxyEndpoint: a.Endpoint, common.HTTPAuthProxyEndpoint: a.Endpoint,
common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint, common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint,
common.HTTPAuthProxyVerifyCert: !a.SkipCertVerify, 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) 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 { if len(userGroupList) > 0 {
return auth.ErrDuplicateLDAPGroup 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 // PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser

View File

@ -97,6 +97,7 @@ func initRouters() {
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post") 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/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/CVEWhitelist", &api.SysCVEWhitelistAPI{}, "get:Get;put:Put")
beego.Router("/api/system/oidc/ping", &api.OIDCAPI{}, "post:Ping")
beego.Router("/api/logs", &api.LogAPI{}) beego.Router("/api/logs", &api.LogAPI{})

View File

@ -21,18 +21,10 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"net/url"
"os"
"strings" "strings"
"time" "time"
"context" "context"
"github.com/goharbor/harbor/src/jobservice/common/utils"
)
const (
proxyEnvHTTP = "http_proxy"
proxyEnvHTTPS = "https_proxy"
) )
// Client for handling the hook events // Client for handling the hook events
@ -60,19 +52,7 @@ func NewClient(ctx context.Context) Client {
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second, ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
} Proxy: http.ProxyFromEnvironment,
// 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)
}
} }
client := &http.Client{ client := &http.Client{

View File

@ -99,6 +99,7 @@
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button> class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
</div> </div>
<div class="add-modal" *ngIf="showAddModal"> <div class="add-modal" *ngIf="showAddModal">
<clr-icon (click)="showAddModal=false" class="float-lg-right margin-top-4" shape="window-close"></clr-icon>
<div> <div>
<clr-textarea-container> <clr-textarea-container>
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label> <label>{{'CVE_WHITELIST.ENTER'|translate}}</label>

View File

@ -11,17 +11,17 @@
</clr-tooltip> </clr-tooltip>
<clr-dropdown *ngIf="isClairDBFullyReady && showScanningNamespaces" class="clr-dropdown-override"> <clr-dropdown *ngIf="isClairDBFullyReady && showScanningNamespaces" class="clr-dropdown-override">
<button class="btn btn-link btn-font" clrDropdownToggle> <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> <clr-icon shape="caret down"></clr-icon>
</button> </button>
<clr-dropdown-menu [clrPosition]="'bottom-right'" class="dropdown-namespace"> <clr-dropdown-menu [clrPosition]="'bottom-right'" class="dropdown-namespace">
<div *ngFor="let nt of namespaceTimestamps" class="namespace"> <div *ngFor="let nt of namespaceTimestamps" class="namespace">
<span class="label label-info">{{nt.namespace}}</span> <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> </div>
</clr-dropdown-menu> </clr-dropdown-menu>
</clr-dropdown> </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>
<div class="button-group"> <div class="button-group">
<cron-selection #CronScheduleComponent [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent' [originCron]='originCron' (inputvalue)="scanAll($event)"></cron-selection> <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> class="btn btn-link ml-1">{{'CVE_WHITELIST.ADD_SYSTEM'|translate}}</button>
</div> </div>
<div class="add-modal" *ngIf="showAddModal && !isUseSystemWhitelist()"> <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> <div>
<clr-textarea-container> <clr-textarea-container>
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label> <label>{{'CVE_WHITELIST.ENTER'|translate}}</label>

View File

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

View File

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

View File

@ -322,7 +322,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.", "CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'", "COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts", "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": "Group", "GROUP": "Group",

View File

@ -323,7 +323,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.", "CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'", "COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts", "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": "Group", "GROUP": "Group",

View File

@ -315,7 +315,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.", "CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'", "COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ", "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": "Group", "Group": "Group",

View File

@ -320,7 +320,8 @@
"CREATED_SUCCESS": "Created '{{param}}' successfully.", "CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'", "COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirmar a remoção do robô Contas", "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": {
"GROUP": "Grupo", "GROUP": "Grupo",

View File

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