mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-20 07:37:38 +01:00
commit
1e2660f060
4
Makefile
4
Makefile
@ -292,7 +292,7 @@ compile_notary_migrate_patch:
|
||||
compile: check_environment versions_prepare compile_core compile_jobservice compile_registryctl compile_notary_migrate_patch
|
||||
|
||||
update_prepare_version:
|
||||
@echo "substitude the prepare version tag in prepare file..."
|
||||
@echo "substitute the prepare version tag in prepare file..."
|
||||
@$(SEDCMD) -i -e 's/goharbor\/prepare:.*[[:space:]]\+/goharbor\/prepare:$(VERSIONTAG) /' $(MAKEPATH)/prepare ;
|
||||
|
||||
prepare: update_prepare_version
|
||||
@ -416,7 +416,7 @@ start:
|
||||
@echo "Start complete. You can visit harbor now."
|
||||
|
||||
down:
|
||||
@echo "Please make sure to set -e NOTARYFLAG=true/CLAIRFLAG=true/CHARTFLAG=true if you are using Notary/CLAIR/Chartmuseum in Harbor, otherwise the Notary/CLAIR/Chartmuseum containers cannot be stop automaticlly."
|
||||
@echo "Please make sure to set -e NOTARYFLAG=true/CLAIRFLAG=true/CHARTFLAG=true if you are using Notary/CLAIR/Chartmuseum in Harbor, otherwise the Notary/CLAIR/Chartmuseum containers cannot be stopped automatically."
|
||||
@while [ -z "$$CONTINUE" ]; do \
|
||||
read -r -p "Type anything but Y or y to exit. [Y/N]: " CONTINUE; \
|
||||
done ; \
|
||||
|
@ -44,25 +44,25 @@ You can compile the code by one of the three approaches:
|
||||
* Get official Golang image from docker hub:
|
||||
|
||||
```sh
|
||||
$ docker pull golang:1.11.2
|
||||
$ docker pull golang:1.12.5
|
||||
```
|
||||
|
||||
* Build, install and bring up Harbor without Notary:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.11.2 COMPILETAG=compile_golangimage
|
||||
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage
|
||||
```
|
||||
|
||||
* Build, install and bring up Harbor with Notary:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.11.2 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
```
|
||||
|
||||
* Build, install and bring up Harbor with Clair:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.11.2 COMPILETAG=compile_golangimage CLAIRFLAG=true
|
||||
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage CLAIRFLAG=true
|
||||
```
|
||||
|
||||
#### II. Compile code with your own Golang environment, then build Harbor
|
||||
|
@ -17,18 +17,23 @@ This guide provides instructions to manage roles by LDAP/AD group. You can impor
|
||||
|
||||
Besides **[basic LDAP configure parameters](https://github.com/vmware/harbor/blob/master/docs/installation_guide.md#optional-parameters)** , LDAP group related configure parameters should be configured, they can be configured before or after installation
|
||||
|
||||
1. Configure parameters in harbor.cfg before installation
|
||||
1. Configure LDAP parameters via API, refer to **[Config Harbor user settings by command line](configure_user_settings.md)**
|
||||
|
||||
For example:
|
||||
```
|
||||
curl -X PUT -u "<username>:<password>" -H "Content-Type: application/json" -ki https://harbor.sample.domain/api/configurations -d'{"ldap_group_basedn":"ou=groups,dc=example,dc=com"}'
|
||||
```
|
||||
The following parameters are related to LDAP group configuration.
|
||||
* ldap_group_basedn -- The base DN from which to lookup a group in LDAP/AD, for example: ou=groups,dc=example,dc=com
|
||||
* ldap_group_filter -- The filter to search LDAP/AD group, for example: objectclass=groupOfNames
|
||||
* ldap_group_gid -- The attribute used to name an LDAP/AD group, for example: cn
|
||||
* ldap_group_scope -- The scope to search for LDAP/AD groups. 0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE
|
||||
|
||||
2. Or Change configure parameter in web console after installation. Go to "Administration" -> "Configuration" -> "Authentication" and change following settings.
|
||||
- LDAP Group Base DN -- ldap_group_basedn in harbor.cfg
|
||||
- LDAP Group Filter -- ldap_group_filter in harbor.cfg
|
||||
- LDAP Group GID -- ldap_group_gid in harbor.cfg
|
||||
- LDAP Group Scope -- ldap_group_scope in harbor.cfg
|
||||
2. Or change configure parameter in web console after installation. Go to "Administration" -> "Configuration" -> "Authentication" and change following settings.
|
||||
- LDAP Group Base DN -- ldap_group_basedn in the Harbor user settings
|
||||
- LDAP Group Filter -- ldap_group_filter in the Harbor user settings
|
||||
- LDAP Group GID -- ldap_group_gid in the Harbor user settings
|
||||
- LDAP Group Scope -- ldap_group_scope in the Harbor user settings
|
||||
- LDAP Groups With Admin Privilege -- Specify an LDAP/AD group DN, all LDAPA/AD users in this group have harbor admin privileges.
|
||||
|
||||
![Screenshot of LDAP group config](img/group/ldap_group_config.png)
|
||||
|
@ -2,7 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: Harbor API
|
||||
description: These APIs provide services for manipulating Harbor project.
|
||||
version: 1.8.0
|
||||
version: 1.9.0
|
||||
host: localhost
|
||||
schemes:
|
||||
- http
|
||||
@ -3478,6 +3478,44 @@ paths:
|
||||
description: The robot account is not found.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/system/CVEWhitelist':
|
||||
get:
|
||||
summary: Get the system level whitelist of CVE.
|
||||
description: Get the system level whitelist of CVE. This API can be called by all authenticated users.
|
||||
tags:
|
||||
- Products
|
||||
- System
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved the CVE whitelist.
|
||||
schema:
|
||||
$ref: "#/definitions/CVEWhitelist"
|
||||
'401':
|
||||
description: User is not authenticated.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
put:
|
||||
summary: Update the system level whitelist of CVE.
|
||||
description: This API overwrites the system level whitelist of CVE with the list in request body. Only system Admin
|
||||
has permission to call this API.
|
||||
tags:
|
||||
- Products
|
||||
- System
|
||||
parameters:
|
||||
- in: body
|
||||
name: whitelist
|
||||
description: The whitelist with new content
|
||||
schema:
|
||||
$ref: "#/definitions/CVEWhitelist"
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully updated the CVE whitelist.
|
||||
'401':
|
||||
description: User is not authenticated.
|
||||
'403':
|
||||
description: User does not have permission to call this API.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
responses:
|
||||
OK:
|
||||
description: 'Success'
|
||||
@ -3601,6 +3639,9 @@ definitions:
|
||||
metadata:
|
||||
description: The metadata of the project.
|
||||
$ref: '#/definitions/ProjectMetadata'
|
||||
cve_whitelist:
|
||||
description: The CVE whitelist of this project.
|
||||
$ref: '#/definitions/CVEWhitelist'
|
||||
ProjectMetadata:
|
||||
type: object
|
||||
properties:
|
||||
@ -5069,4 +5110,28 @@ definitions:
|
||||
description: The name of namespace
|
||||
metadata:
|
||||
type: object
|
||||
description: The metadata of namespace
|
||||
description: The metadata of namespace
|
||||
CVEWhitelist:
|
||||
type: object
|
||||
description: The CVE Whitelist for system or project
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: ID of the whitelist
|
||||
project_id:
|
||||
type: integer
|
||||
description: ID of the project which the whitelist belongs to. For system level whitelist this attribute is zero.
|
||||
expires_at:
|
||||
type: integer
|
||||
description: the time for expiration of the whitelist, in the form of seconds since epoch. This is an optional attribute, if it's not set the CVE whitelist does not expire.
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/CVEWhitelistItem"
|
||||
CVEWhitelistItem:
|
||||
type: object
|
||||
description: The item in CVE whitelist
|
||||
properties:
|
||||
cve_id:
|
||||
type: string
|
||||
description: The ID of the CVE, such as "CVE-2019-10164"
|
||||
|
@ -36,10 +36,10 @@ version | set harbor version
|
||||
#### EXAMPLE:
|
||||
|
||||
#### Build and run harbor from source code.
|
||||
make install GOBUILDIMAGE=golang:1.11.2 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
|
||||
### Package offline installer
|
||||
make package_offline GOBUILDIMAGE=golang:1.11.2 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
make package_offline GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
|
||||
### Start harbor with notary
|
||||
make -e NOTARYFLAG=true start
|
||||
|
10
make/migrations/postgresql/0010_1.9.0_schema.up.sql
Normal file
10
make/migrations/postgresql/0010_1.9.0_schema.up.sql
Normal file
@ -0,0 +1,10 @@
|
||||
/* add table for CVE whitelist */
|
||||
CREATE TABLE cve_whitelist (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
project_id int,
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP,
|
||||
expires_at bigint,
|
||||
items text NOT NULL,
|
||||
UNIQUE (project_id)
|
||||
);
|
@ -8,5 +8,6 @@ RUN mkdir /harbor/ \
|
||||
COPY ./make/photon/jobservice/start.sh ./make/photon/jobservice/harbor_jobservice /harbor/
|
||||
|
||||
RUN chmod u+x /harbor/harbor_jobservice /harbor/start.sh
|
||||
RUN mkdir -p /var/log/jobs
|
||||
WORKDIR /harbor/
|
||||
ENTRYPOINT ["/harbor/start.sh"]
|
||||
|
74
src/common/dao/cve_whitelist.go
Normal file
74
src/common/dao/cve_whitelist.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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 dao
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// UpdateCVEWhitelist Updates the vulnerability white list to DB
|
||||
func UpdateCVEWhitelist(l models.CVEWhitelist) (int64, error) {
|
||||
o := GetOrmer()
|
||||
itemsBytes, _ := json.Marshal(l.Items)
|
||||
l.ItemsText = string(itemsBytes)
|
||||
id, err := o.InsertOrUpdate(&l, "project_id")
|
||||
return id, err
|
||||
}
|
||||
|
||||
// GetSysCVEWhitelist Gets the system level vulnerability white list from DB
|
||||
func GetSysCVEWhitelist() (*models.CVEWhitelist, error) {
|
||||
return GetCVEWhitelist(0)
|
||||
}
|
||||
|
||||
// UpdateSysCVEWhitelist updates the system level CVE whitelist
|
||||
/*
|
||||
func UpdateSysCVEWhitelist(l models.CVEWhitelist) error {
|
||||
if l.ProjectID != 0 {
|
||||
return fmt.Errorf("system level CVE whitelist cannot set project ID")
|
||||
}
|
||||
l.ProjectID = -1
|
||||
_, err := UpdateCVEWhitelist(l)
|
||||
return err
|
||||
}
|
||||
*/
|
||||
|
||||
// GetCVEWhitelist Gets the CVE whitelist of the project based on the project ID in parameter
|
||||
func GetCVEWhitelist(pid int64) (*models.CVEWhitelist, error) {
|
||||
o := GetOrmer()
|
||||
qs := o.QueryTable(&models.CVEWhitelist{})
|
||||
qs = qs.Filter("ProjectID", pid)
|
||||
r := []*models.CVEWhitelist{}
|
||||
_, err := qs.All(&r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get CVE whitelist for project %d, error: %v", pid, err)
|
||||
}
|
||||
if len(r) == 0 {
|
||||
log.Infof("No CVE whitelist found for project %d, returning empty list.", pid)
|
||||
return &models.CVEWhitelist{ProjectID: pid, Items: []models.CVEWhitelistItem{}}, nil
|
||||
} else if len(r) > 1 {
|
||||
log.Infof("Multiple CVE whitelists found for project %d, length: %d, returning first element.", pid, len(r))
|
||||
}
|
||||
items := []models.CVEWhitelistItem{}
|
||||
err = json.Unmarshal([]byte(r[0].ItemsText), &items)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to decode item list, err: %v, text: %s", err, r[0].ItemsText)
|
||||
return nil, err
|
||||
}
|
||||
r[0].Items = items
|
||||
return r[0], nil
|
||||
}
|
72
src/common/dao/cve_whitelist_test.go
Normal file
72
src/common/dao/cve_whitelist_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
// 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 dao
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdateAndGetCVEWhitelist(t *testing.T) {
|
||||
require.Nil(t, ClearTable("cve_whitelist"))
|
||||
l, err := GetSysCVEWhitelist()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, models.CVEWhitelist{ProjectID: 0, Items: []models.CVEWhitelistItem{}}, *l)
|
||||
l2, err := GetCVEWhitelist(5)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, models.CVEWhitelist{ProjectID: 5, Items: []models.CVEWhitelistItem{}}, *l2)
|
||||
|
||||
longList := []models.CVEWhitelistItem{}
|
||||
for i := 0; i < 50; i++ {
|
||||
longList = append(longList, models.CVEWhitelistItem{CVEID: "CVE-1999-0067"})
|
||||
}
|
||||
|
||||
e := int64(1573254000)
|
||||
in1 := models.CVEWhitelist{ProjectID: 3, Items: longList, ExpiresAt: &e}
|
||||
_, err = UpdateCVEWhitelist(in1)
|
||||
require.Nil(t, err)
|
||||
// assert.Equal(t, int64(1), n)
|
||||
out1, err := GetCVEWhitelist(3)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, int64(3), out1.ProjectID)
|
||||
assert.Equal(t, longList, out1.Items)
|
||||
assert.Equal(t, e, *out1.ExpiresAt)
|
||||
|
||||
in2 := models.CVEWhitelist{ProjectID: 3, Items: []models.CVEWhitelistItem{}}
|
||||
_, err = UpdateCVEWhitelist(in2)
|
||||
require.Nil(t, err)
|
||||
// assert.Equal(t, int64(1), n2)
|
||||
out2, err := GetCVEWhitelist(3)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, int64(3), out2.ProjectID)
|
||||
assert.Equal(t, []models.CVEWhitelistItem{}, out2.Items)
|
||||
|
||||
sysCVEs := []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2019-10164"},
|
||||
{CVEID: "CVE-2017-12345"},
|
||||
}
|
||||
in3 := models.CVEWhitelist{Items: sysCVEs}
|
||||
_, err = UpdateCVEWhitelist(in3)
|
||||
require.Nil(t, err)
|
||||
// assert.Equal(t, int64(1), n3)
|
||||
sysList, err := GetSysCVEWhitelist()
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, int64(0), sysList.ProjectID)
|
||||
assert.Equal(t, sysCVEs, sysList.Items)
|
||||
|
||||
// require.Nil(t, ClearTable("cve_whitelist"))
|
||||
}
|
@ -15,10 +15,8 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
@ -139,20 +137,3 @@ func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttri
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGroupDNQueryCondition get the part of IN ('XXX', 'XXX') condition
|
||||
func GetGroupDNQueryCondition(userGroupList []*models.UserGroup) string {
|
||||
result := make([]string, 0)
|
||||
count := 0
|
||||
for _, userGroup := range userGroupList {
|
||||
if userGroup.GroupType == common.LdapGroupType {
|
||||
result = append(result, "'"+userGroup.LdapGroupDN+"'")
|
||||
count++
|
||||
}
|
||||
}
|
||||
// No LDAP Group found
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
@ -47,6 +47,8 @@ func TestMain(m *testing.M) {
|
||||
initSqls := []string{
|
||||
"insert into harbor_user (username, email, password, realname) values ('member_test_01', 'member_test_01@example.com', '123456', 'member_test_01')",
|
||||
"insert into project (name, owner_id) values ('member_test_01', 1)",
|
||||
`insert into project (name, owner_id) values ('group_project2', 1)`,
|
||||
`insert into project (name, owner_id) values ('group_project_private', 1)`,
|
||||
"insert into user_group (group_name, group_type, ldap_group_dn) values ('test_group_01', 1, 'cn=harbor_users,ou=sample,ou=vmware,dc=harbor,dc=com')",
|
||||
"update project set owner_id = (select user_id from harbor_user where username = 'member_test_01') where name = 'member_test_01'",
|
||||
"insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_01') , (select user_id from harbor_user where username = 'member_test_01'), 'u', 1)",
|
||||
@ -55,6 +57,8 @@ func TestMain(m *testing.M) {
|
||||
|
||||
clearSqls := []string{
|
||||
"delete from project where name='member_test_01'",
|
||||
"delete from project where name='group_project2'",
|
||||
"delete from project where name='group_project_private'",
|
||||
"delete from harbor_user where username='member_test_01' or username='pm_sample'",
|
||||
"delete from user_group",
|
||||
"delete from project_member",
|
||||
@ -175,7 +179,7 @@ func TestUpdateUserGroup(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fmt.Printf("id=%v", createdUserGroupID)
|
||||
fmt.Printf("id=%v\n", createdUserGroupID)
|
||||
if err := UpdateUserGroupName(tt.args.id, tt.args.groupName); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UpdateUserGroup() error = %v, wantErr %v", err, tt.wantErr)
|
||||
userGroup, err := GetUserGroup(tt.args.id)
|
||||
@ -249,40 +253,6 @@ func TestOnBoardUserGroup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroupDNQueryCondition(t *testing.T) {
|
||||
userGroupList := []*models.UserGroup{
|
||||
{
|
||||
GroupName: "sample1",
|
||||
GroupType: 1,
|
||||
LdapGroupDN: "cn=sample1_users,ou=groups,dc=example,dc=com",
|
||||
},
|
||||
{
|
||||
GroupName: "sample2",
|
||||
GroupType: 1,
|
||||
LdapGroupDN: "cn=sample2_users,ou=groups,dc=example,dc=com",
|
||||
},
|
||||
{
|
||||
GroupName: "sample3",
|
||||
GroupType: 0,
|
||||
LdapGroupDN: "cn=sample3_users,ou=groups,dc=example,dc=com",
|
||||
},
|
||||
}
|
||||
|
||||
groupQueryConditions := GetGroupDNQueryCondition(userGroupList)
|
||||
expectedConditions := `'cn=sample1_users,ou=groups,dc=example,dc=com','cn=sample2_users,ou=groups,dc=example,dc=com'`
|
||||
if groupQueryConditions != expectedConditions {
|
||||
t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", expectedConditions, groupQueryConditions)
|
||||
}
|
||||
var userGroupList2 []*models.UserGroup
|
||||
groupQueryCondition2 := GetGroupDNQueryCondition(userGroupList2)
|
||||
if len(groupQueryCondition2) > 0 {
|
||||
t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", "", groupQueryCondition2)
|
||||
}
|
||||
groupQueryCondition3 := GetGroupDNQueryCondition(nil)
|
||||
if len(groupQueryCondition3) > 0 {
|
||||
t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", "", groupQueryCondition3)
|
||||
}
|
||||
}
|
||||
func TestGetGroupProjects(t *testing.T) {
|
||||
userID, err := dao.Register(models.User{
|
||||
Username: "grouptestu09",
|
||||
@ -322,8 +292,7 @@ func TestGetGroupProjects(t *testing.T) {
|
||||
})
|
||||
defer project.DeleteProjectMemberByID(pmid)
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
member := &models.MemberQuery{
|
||||
Name: "grouptestu09",
|
||||
@ -335,19 +304,17 @@ func TestGetGroupProjects(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{"Query with group DN",
|
||||
args{"'cn=harbor_users,ou=groups,dc=example,dc=com'",
|
||||
&models.ProjectQueryParam{
|
||||
Member: member,
|
||||
}},
|
||||
args{&models.ProjectQueryParam{
|
||||
Member: member,
|
||||
}},
|
||||
1, false},
|
||||
{"Query without group DN",
|
||||
args{"",
|
||||
&models.ProjectQueryParam{}},
|
||||
args{&models.ProjectQueryParam{}},
|
||||
1, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := dao.GetGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
got, err := dao.GetGroupProjects([]int{groupID}, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@ -392,8 +359,7 @@ func TestGetTotalGroupProjects(t *testing.T) {
|
||||
})
|
||||
defer project.DeleteProjectMemberByID(pmid)
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -401,18 +367,16 @@ func TestGetTotalGroupProjects(t *testing.T) {
|
||||
wantSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Query with group DN",
|
||||
args{"'cn=harbor_users,ou=groups,dc=example,dc=com'",
|
||||
&models.ProjectQueryParam{}},
|
||||
{"Query with group ID",
|
||||
args{&models.ProjectQueryParam{}},
|
||||
1, false},
|
||||
{"Query without group DN",
|
||||
args{"",
|
||||
&models.ProjectQueryParam{}},
|
||||
{"Query without group ID",
|
||||
args{&models.ProjectQueryParam{}},
|
||||
1, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := dao.GetTotalGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
got, err := dao.GetTotalGroupProjects([]int{groupID}, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@ -423,3 +387,44 @@ func TestGetTotalGroupProjects(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestGetRolesByLDAPGroup(t *testing.T) {
|
||||
|
||||
userGroupList, err := QueryUserGroup(models.UserGroup{LdapGroupDN: "cn=harbor_users,ou=sample,ou=vmware,dc=harbor,dc=com", GroupType: 1})
|
||||
if err != nil || len(userGroupList) < 1 {
|
||||
t.Errorf("failed to query user group, err %v", err)
|
||||
}
|
||||
project, err := dao.GetProjectByName("member_test_01")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when Get project by name: %v", err)
|
||||
}
|
||||
privateProject, err := dao.GetProjectByName("group_project_private")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when Get project by name: %v", err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
projectID int64
|
||||
groupIDs []int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Check normal", args{projectID: project.ProjectID, groupIDs: []int{userGroupList[0].ID}}, 1, false},
|
||||
{"Check non exist", args{projectID: privateProject.ProjectID, groupIDs: []int{9999}}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := dao.GetRolesByGroupID(tt.args.projectID, tt.args.groupIDs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TestGetRolesByLDAPGroup() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if len(got) != tt.wantSize {
|
||||
t.Errorf("TestGetRolesByLDAPGroup() = %v, want %v", len(got), tt.wantSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -156,18 +156,19 @@ func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
|
||||
// GetGroupProjects - Get user's all projects, including user is the user member of this project
|
||||
// and the user is in the group which is a group member of this project.
|
||||
func GetGroupProjects(groupDNCondition string, query *models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
func GetGroupProjects(groupIDs []int, query *models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
sql, params := projectQueryConditions(query)
|
||||
sql = `select distinct p.project_id, p.name, p.owner_id,
|
||||
p.creation_time, p.update_time ` + sql
|
||||
if len(groupDNCondition) > 0 {
|
||||
groupIDCondition := JoinNumberConditions(groupIDs)
|
||||
if len(groupIDs) > 0 {
|
||||
sql = fmt.Sprintf(
|
||||
`%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time
|
||||
from project p
|
||||
left join project_member pm on p.project_id = pm.project_id
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' and ug.group_type = 1
|
||||
where ug.ldap_group_dn in ( %s ) order by name`,
|
||||
sql, groupDNCondition)
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
|
||||
where ug.id in ( %s ) order by name`,
|
||||
sql, groupIDCondition)
|
||||
}
|
||||
sqlStr, queryParams := CreatePagination(query, sql, params)
|
||||
log.Debugf("query sql:%v", sql)
|
||||
@ -178,10 +179,11 @@ func GetGroupProjects(groupDNCondition string, query *models.ProjectQueryParam)
|
||||
|
||||
// GetTotalGroupProjects - Get the total count of projects, including user is the member of this project and the
|
||||
// user is in the group, which is the group member of this project.
|
||||
func GetTotalGroupProjects(groupDNCondition string, query *models.ProjectQueryParam) (int, error) {
|
||||
func GetTotalGroupProjects(groupIDs []int, query *models.ProjectQueryParam) (int, error) {
|
||||
var sql string
|
||||
sqlCondition, params := projectQueryConditions(query)
|
||||
if len(groupDNCondition) == 0 {
|
||||
groupIDCondition := JoinNumberConditions(groupIDs)
|
||||
if len(groupIDs) == 0 {
|
||||
sql = `select count(1) ` + sqlCondition
|
||||
} else {
|
||||
sql = fmt.Sprintf(
|
||||
@ -189,9 +191,9 @@ func GetTotalGroupProjects(groupDNCondition string, query *models.ProjectQueryPa
|
||||
from ( select p.project_id %s union select p.project_id
|
||||
from project p
|
||||
left join project_member pm on p.project_id = pm.project_id
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' and ug.group_type = 1
|
||||
where ug.ldap_group_dn in ( %s )) t`,
|
||||
sqlCondition, groupDNCondition)
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
|
||||
where ug.id in ( %s )) t`,
|
||||
sqlCondition, groupIDCondition)
|
||||
}
|
||||
log.Debugf("query sql:%v", sql)
|
||||
var count int
|
||||
@ -291,24 +293,24 @@ func DeleteProject(id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRolesByLDAPGroup - Get Project roles of the
|
||||
// specified group DN is a member of current project
|
||||
func GetRolesByLDAPGroup(projectID int64, groupDNCondition string) ([]int, error) {
|
||||
// GetRolesByGroupID - Get Project roles of the
|
||||
// specified group is a member of current project
|
||||
func GetRolesByGroupID(projectID int64, groupIDs []int) ([]int, error) {
|
||||
var roles []int
|
||||
if len(groupDNCondition) == 0 {
|
||||
if len(groupIDs) == 0 {
|
||||
return roles, nil
|
||||
}
|
||||
groupIDCondition := JoinNumberConditions(groupIDs)
|
||||
o := GetOrmer()
|
||||
// Because an LDAP user can be memberof multiple groups,
|
||||
// the role is in descent order (1-admin, 2-developer, 3-guest, 4-master), use min to select the max privilege role.
|
||||
sql := fmt.Sprintf(
|
||||
`select min(pm.role) from project_member pm
|
||||
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id
|
||||
where ug.ldap_group_dn in ( %s ) and pm.project_id = ? `,
|
||||
groupDNCondition)
|
||||
where ug.id in ( %s ) and pm.project_id = ?`,
|
||||
groupIDCondition)
|
||||
log.Debugf("sql:%v", sql)
|
||||
if _, err := o.Raw(sql, projectID).QueryRows(&roles); err != nil {
|
||||
log.Warningf("Error in GetRolesByLDAPGroup, error: %v", err)
|
||||
log.Warningf("Error in GetRolesByGroupID, error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// If there is no row selected, the min returns an empty row, to avoid return 0 as role
|
||||
|
@ -148,16 +148,3 @@ func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, e
|
||||
_, err := o.Raw(sql, queryParam).QueryRows(&members)
|
||||
return members, err
|
||||
}
|
||||
|
||||
// GetRolesByGroup -- Query group roles
|
||||
func GetRolesByGroup(projectID int64, groupDNCondition string) []int {
|
||||
var roles []int
|
||||
o := dao.GetOrmer()
|
||||
sql := `select role from project_member pm
|
||||
left join user_group ug on pm.project_id = ?
|
||||
where ug.group_type = 1 and ug.ldap_group_dn in (` + groupDNCondition + `)`
|
||||
if _, err := o.Raw(sql, projectID).QueryRows(&roles); err != nil {
|
||||
return roles
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
@ -305,30 +305,3 @@ func PrepareGroupTest() {
|
||||
}
|
||||
dao.PrepareTestData(clearSqls, initSqls)
|
||||
}
|
||||
func TestGetRolesByGroup(t *testing.T) {
|
||||
PrepareGroupTest()
|
||||
|
||||
project, err := dao.GetProjectByName("group_project")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when GetProjectByName : %v", err)
|
||||
}
|
||||
type args struct {
|
||||
projectID int64
|
||||
groupDNCondition string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []int
|
||||
}{
|
||||
{"Query group with role", args{project.ProjectID, "'cn=harbor_user,dc=example,dc=com'"}, []int{2}},
|
||||
{"Query group no role", args{project.ProjectID, "'cn=another_user,dc=example,dc=com'"}, []int{}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetRolesByGroup(tt.args.projectID, tt.args.groupDNCondition); !dao.ArrayEqual(got, tt.want) {
|
||||
t.Errorf("GetRolesByGroup() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -118,36 +118,6 @@ func Test_projectQueryConditions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroupProjects(t *testing.T) {
|
||||
prepareGroupTest()
|
||||
query := &models.ProjectQueryParam{Member: &models.MemberQuery{Name: "sample_group"}}
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Verify correct sql", args{groupDNCondition: "'cn=harbor_user,dc=example,dc=com'", query: query}, 1, false},
|
||||
{"Verify missed sql", args{groupDNCondition: "'cn=another_user,dc=example,dc=com'", query: query}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if len(got) != tt.wantSize {
|
||||
t.Errorf("GetGroupProjects() = %v, want %v", got, tt.wantSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func prepareGroupTest() {
|
||||
initSqls := []string{
|
||||
`insert into user_group (group_name, group_type, ldap_group_dn) values ('harbor_group_01', 1, 'cn=harbor_user,dc=example,dc=com')`,
|
||||
@ -169,73 +139,6 @@ func prepareGroupTest() {
|
||||
PrepareTestData(clearSqls, initSqls)
|
||||
}
|
||||
|
||||
func TestGetTotalGroupProjects(t *testing.T) {
|
||||
prepareGroupTest()
|
||||
query := &models.ProjectQueryParam{Member: &models.MemberQuery{Name: "sample_group"}}
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Verify correct sql", args{groupDNCondition: "'cn=harbor_user,dc=example,dc=com'", query: query}, 1, false},
|
||||
{"Verify missed sql", args{groupDNCondition: "'cn=another_user,dc=example,dc=com'", query: query}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetTotalGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTotalGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GetTotalGroupProjects() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRolesByLDAPGroup(t *testing.T) {
|
||||
prepareGroupTest()
|
||||
project, err := GetProjectByName("group_project")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when Get project by name: %v", err)
|
||||
}
|
||||
privateProject, err := GetProjectByName("group_project_private")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when Get project by name: %v", err)
|
||||
}
|
||||
type args struct {
|
||||
projectID int64
|
||||
groupDNCondition string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Check normal", args{project.ProjectID, "'cn=harbor_user,dc=example,dc=com'"}, 1, false},
|
||||
{"Check non exist", args{privateProject.ProjectID, "'cn=not_harbor_user,dc=example,dc=com'"}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetRolesByLDAPGroup(tt.args.projectID, tt.args.groupDNCondition)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TestGetRolesByLDAPGroup() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if len(got) != tt.wantSize {
|
||||
t.Errorf("TestGetRolesByLDAPGroup() = %v, want %v", len(got), tt.wantSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjetExistsByName(t *testing.T) {
|
||||
name := "project_exist_by_name_test"
|
||||
exist := ProjectExistsByName(name)
|
||||
|
@ -15,12 +15,11 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
11
src/common/dao/utils.go
Normal file
11
src/common/dao/utils.go
Normal file
@ -0,0 +1,11 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JoinNumberConditions - To join number condition into string,used in sql query
|
||||
func JoinNumberConditions(ids []int) string {
|
||||
return strings.Trim(strings.Replace(fmt.Sprint(ids), " ", ",", -1), "[]")
|
||||
}
|
24
src/common/dao/utils_test.go
Normal file
24
src/common/dao/utils_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package dao
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinNumberConditions(t *testing.T) {
|
||||
type args struct {
|
||||
ids []int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{name: "normal test", args: args{[]int{1, 2, 3}}, want: "1,2,3"},
|
||||
{name: "dummy test", args: args{[]int{}}, want: ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := JoinNumberConditions(tt.args.ids); got != tt.want {
|
||||
t.Errorf("JoinNumberConditions() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -36,5 +36,6 @@ func init() {
|
||||
new(AdminJob),
|
||||
new(JobLog),
|
||||
new(Robot),
|
||||
new(OIDCUser))
|
||||
new(OIDCUser),
|
||||
new(CVEWhitelist))
|
||||
}
|
||||
|
55
src/common/models/cve_whitelist.go
Normal file
55
src/common/models/cve_whitelist.go
Normal file
@ -0,0 +1,55 @@
|
||||
// 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 models
|
||||
|
||||
import "time"
|
||||
|
||||
// CVEWhitelist defines the data model for a CVE whitelist
|
||||
type CVEWhitelist struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
|
||||
ExpiresAt *int64 `orm:"column(expires_at)" json:"expires_at,omitempty"`
|
||||
Items []CVEWhitelistItem `orm:"-" json:"items"`
|
||||
ItemsText string `orm:"column(items)" json:"-"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// CVEWhitelistItem defines one item in the CVE whitelist
|
||||
type CVEWhitelistItem struct {
|
||||
CVEID string `json:"cve_id"`
|
||||
}
|
||||
|
||||
// TableName ...
|
||||
func (c *CVEWhitelist) TableName() string {
|
||||
return "cve_whitelist"
|
||||
}
|
||||
|
||||
// CVESet returns the set of CVE id of the items in the whitelist to help filter the vulnerability list
|
||||
func (c *CVEWhitelist) CVESet() map[string]struct{} {
|
||||
r := map[string]struct{}{}
|
||||
for _, it := range c.Items {
|
||||
r[it.CVEID] = struct{}{}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// IsExpired returns whether the whitelist is expired
|
||||
func (c *CVEWhitelist) IsExpired() bool {
|
||||
if c.ExpiresAt == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().Unix() >= *c.ExpiresAt
|
||||
}
|
72
src/common/models/cve_whitelist_test.go
Normal file
72
src/common/models/cve_whitelist_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCVEWhitelist_All(t *testing.T) {
|
||||
future := int64(4411494000)
|
||||
now := time.Now().Unix()
|
||||
cases := []struct {
|
||||
input CVEWhitelist
|
||||
cveset map[string]struct{}
|
||||
expired bool
|
||||
}{
|
||||
{
|
||||
input: CVEWhitelist{
|
||||
ID: 1,
|
||||
ProjectID: 0,
|
||||
Items: []CVEWhitelistItem{},
|
||||
},
|
||||
cveset: map[string]struct{}{},
|
||||
expired: false,
|
||||
},
|
||||
{
|
||||
input: CVEWhitelist{
|
||||
ID: 1,
|
||||
ProjectID: 0,
|
||||
Items: []CVEWhitelistItem{},
|
||||
ExpiresAt: &now,
|
||||
},
|
||||
cveset: map[string]struct{}{},
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
input: CVEWhitelist{
|
||||
ID: 2,
|
||||
ProjectID: 3,
|
||||
Items: []CVEWhitelistItem{
|
||||
{CVEID: "CVE-1999-0067"},
|
||||
{CVEID: "CVE-2016-7654321"},
|
||||
},
|
||||
ExpiresAt: &future,
|
||||
},
|
||||
cveset: map[string]struct{}{
|
||||
"CVE-1999-0067": {},
|
||||
"CVE-2016-7654321": {},
|
||||
},
|
||||
expired: false,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expired, c.input.IsExpired())
|
||||
assert.True(t, reflect.DeepEqual(c.cveset, c.input.CVESet()))
|
||||
}
|
||||
}
|
@ -20,16 +20,17 @@ import (
|
||||
|
||||
// keys of project metadata and severity values
|
||||
const (
|
||||
ProMetaPublic = "public"
|
||||
ProMetaEnableContentTrust = "enable_content_trust"
|
||||
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
|
||||
ProMetaSeverity = "severity"
|
||||
ProMetaAutoScan = "auto_scan"
|
||||
SeverityNone = "negligible"
|
||||
SeverityLow = "low"
|
||||
SeverityMedium = "medium"
|
||||
SeverityHigh = "high"
|
||||
SeverityCritical = "critical"
|
||||
ProMetaPublic = "public"
|
||||
ProMetaEnableContentTrust = "enable_content_trust"
|
||||
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
|
||||
ProMetaSeverity = "severity"
|
||||
ProMetaAutoScan = "auto_scan"
|
||||
ProMetaReuseSysCVEWhitelist = "reuse_sys_cve_whitelist"
|
||||
SeverityNone = "negligible"
|
||||
SeverityLow = "low"
|
||||
SeverityMedium = "medium"
|
||||
SeverityHigh = "high"
|
||||
SeverityCritical = "critical"
|
||||
)
|
||||
|
||||
// ProjectMetadata holds the metadata of a project.
|
||||
|
@ -36,6 +36,7 @@ type Project struct {
|
||||
RepoCount int64 `orm:"-" json:"repo_count"`
|
||||
ChartCount uint64 `orm:"-" json:"chart_count"`
|
||||
Metadata map[string]string `orm:"-" json:"metadata"`
|
||||
CVEWhitelist CVEWhitelist `orm:"-" json:"cve_whitelist"`
|
||||
}
|
||||
|
||||
// GetMetadata ...
|
||||
@ -83,6 +84,15 @@ func (p *Project) VulPrevented() bool {
|
||||
return isTrue(prevent)
|
||||
}
|
||||
|
||||
// ReuseSysCVEWhitelist ...
|
||||
func (p *Project) ReuseSysCVEWhitelist() bool {
|
||||
r, ok := p.GetMetadata(ProMetaReuseSysCVEWhitelist)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return isTrue(r)
|
||||
}
|
||||
|
||||
// Severity ...
|
||||
func (p *Project) Severity() string {
|
||||
severity, exist := p.GetMetadata(ProMetaSeverity)
|
||||
@ -128,9 +138,9 @@ type ProjectQueryParam struct {
|
||||
|
||||
// MemberQuery filter by member's username and role
|
||||
type MemberQuery struct {
|
||||
Name string // the username of member
|
||||
Role int // the role of the member has to the project
|
||||
GroupList []*UserGroup // the group list of current user
|
||||
Name string // the username of member
|
||||
Role int // the role of the member has to the project
|
||||
GroupIDs []int // the group ID of current user belongs to
|
||||
}
|
||||
|
||||
// Pagination ...
|
||||
@ -154,9 +164,10 @@ type BaseProjectCollection struct {
|
||||
|
||||
// ProjectRequest holds informations that need for creating project API
|
||||
type ProjectRequest struct {
|
||||
Name string `json:"project_name"`
|
||||
Public *int `json:"public"` // deprecated, reserved for project creation in replication
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Name string `json:"project_name"`
|
||||
Public *int `json:"public"` // deprecated, reserved for project creation in replication
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
CVEWhitelist CVEWhitelist `json:"cve_whitelist"`
|
||||
}
|
||||
|
||||
// ProjectQueryResult ...
|
||||
|
@ -34,31 +34,6 @@ type ScanJob struct {
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// Severity represents the severity of a image/component in terms of vulnerability.
|
||||
type Severity int64
|
||||
|
||||
// Sevxxx is the list of severity of image after scanning.
|
||||
const (
|
||||
_ Severity = iota
|
||||
SevNone
|
||||
SevUnknown
|
||||
SevLow
|
||||
SevMedium
|
||||
SevHigh
|
||||
)
|
||||
|
||||
// String is the output function for sererity variable
|
||||
func (sev Severity) String() string {
|
||||
name := []string{"negligible", "unknown", "low", "medium", "high"}
|
||||
i := int64(sev)
|
||||
switch {
|
||||
case i >= 1 && i <= int64(SevHigh):
|
||||
return name[i-1]
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// TableName is required by by beego orm to map ScanJob to table img_scan_job
|
||||
func (s *ScanJob) TableName() string {
|
||||
return ScanJobTable
|
||||
@ -101,17 +76,6 @@ type ImageScanReq struct {
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
// VulnerabilityItem is an item in the vulnerability result returned by vulnerability details API.
|
||||
type VulnerabilityItem struct {
|
||||
ID string `json:"id"`
|
||||
Severity Severity `json:"severity"`
|
||||
Pkg string `json:"package"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Link string `json:"link"`
|
||||
Fixed string `json:"fixedVersion,omitempty"`
|
||||
}
|
||||
|
||||
// ScanAllPolicy is represent the json request and object for scan all policy, the parm is het
|
||||
type ScanAllPolicy struct {
|
||||
Type string `json:"type"`
|
||||
|
26
src/common/models/sev.go
Normal file
26
src/common/models/sev.go
Normal file
@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
// Severity represents the severity of a image/component in terms of vulnerability.
|
||||
type Severity int64
|
||||
|
||||
// Sevxxx is the list of severity of image after scanning.
|
||||
const (
|
||||
_ Severity = iota
|
||||
SevNone
|
||||
SevUnknown
|
||||
SevLow
|
||||
SevMedium
|
||||
SevHigh
|
||||
)
|
||||
|
||||
// String is the output function for severity variable
|
||||
func (sev Severity) String() string {
|
||||
name := []string{"negligible", "unknown", "low", "medium", "high"}
|
||||
i := int64(sev)
|
||||
switch {
|
||||
case i >= 1 && i <= int64(SevHigh):
|
||||
return name[i-1]
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
@ -35,13 +35,13 @@ type User struct {
|
||||
// to it.
|
||||
Role int `orm:"-" json:"role_id"`
|
||||
// RoleList []Role `json:"role_list"`
|
||||
HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"`
|
||||
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
|
||||
Salt string `orm:"column(salt)" json:"-"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
GroupList []*UserGroup `orm:"-" json:"-"`
|
||||
OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta,omitempty"`
|
||||
HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"`
|
||||
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
|
||||
Salt string `orm:"column(salt)" json:"-"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
GroupIDs []int `orm:"-" json:"-"`
|
||||
OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta,omitempty"`
|
||||
}
|
||||
|
||||
// UserQuery ...
|
||||
|
@ -54,6 +54,7 @@ var (
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
@ -27,6 +27,7 @@ var (
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
@ -105,6 +106,7 @@ var (
|
||||
"master": {
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
|
||||
@ -172,6 +174,7 @@ var (
|
||||
"developer": {
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
@ -221,6 +224,7 @@ var (
|
||||
"guest": {
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
|
@ -17,7 +17,6 @@ package local
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
@ -140,12 +139,11 @@ func (s *SecurityContext) GetRolesByGroup(projectIDOrName interface{}) []int {
|
||||
user := s.user
|
||||
project, err := s.pm.Get(projectIDOrName)
|
||||
// No user, group or project info
|
||||
if err != nil || project == nil || user == nil || len(user.GroupList) == 0 {
|
||||
if err != nil || project == nil || user == nil || len(user.GroupIDs) == 0 {
|
||||
return roles
|
||||
}
|
||||
// Get role by LDAP group
|
||||
groupDNConditions := group.GetGroupDNQueryCondition(user.GroupList)
|
||||
roles, err = dao.GetRolesByLDAPGroup(project.ProjectID, groupDNConditions)
|
||||
// Get role by Group ID
|
||||
roles, err = dao.GetRolesByGroupID(project.ProjectID, user.GroupIDs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -157,8 +155,8 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
|
||||
result, err := s.pm.List(
|
||||
&models.ProjectQueryParam{
|
||||
Member: &models.MemberQuery{
|
||||
Name: s.GetUsername(),
|
||||
GroupList: s.user.GroupList,
|
||||
Name: s.GetUsername(),
|
||||
GroupIDs: s.user.GroupIDs,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
@ -253,9 +254,16 @@ func TestHasPushPullPermWithGroup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when GetUser: %v", err)
|
||||
}
|
||||
developer.GroupList = []*models.UserGroup{
|
||||
{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"},
|
||||
|
||||
userGroups, err := group.QueryUserGroup(models.UserGroup{GroupType: common.LdapGroupType, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to query user group %v", err)
|
||||
}
|
||||
if len(userGroups) < 1 {
|
||||
t.Errorf("Failed to retrieve user group")
|
||||
}
|
||||
|
||||
developer.GroupIDs = []int{userGroups[0].ID}
|
||||
|
||||
resource := rbac.NewProjectNamespace(project.Name).Resource(rbac.ResourceRepository)
|
||||
|
||||
@ -332,9 +340,15 @@ func TestSecurityContext_GetRolesByGroup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when GetUser: %v", err)
|
||||
}
|
||||
developer.GroupList = []*models.UserGroup{
|
||||
{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"},
|
||||
userGroups, err := group.QueryUserGroup(models.UserGroup{GroupType: common.LdapGroupType, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to query user group %v", err)
|
||||
}
|
||||
if len(userGroups) < 1 {
|
||||
t.Errorf("Failed to retrieve user group")
|
||||
}
|
||||
|
||||
developer.GroupIDs = []int{userGroups[0].ID}
|
||||
type fields struct {
|
||||
user *models.User
|
||||
pm promgr.ProjectManager
|
||||
|
@ -35,20 +35,14 @@ const googleEndpoint = "https://accounts.google.com"
|
||||
|
||||
type providerHelper struct {
|
||||
sync.Mutex
|
||||
ep endpoint
|
||||
instance atomic.Value
|
||||
setting atomic.Value
|
||||
}
|
||||
|
||||
type endpoint struct {
|
||||
url string
|
||||
VerifyCert bool
|
||||
instance atomic.Value
|
||||
setting atomic.Value
|
||||
creationTime time.Time
|
||||
}
|
||||
|
||||
func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
if p.instance.Load() != nil {
|
||||
s := p.setting.Load().(models.OIDCSetting)
|
||||
if s.Endpoint != p.ep.url || s.VerifyCert != p.ep.VerifyCert { // relevant settings have changed, need to re-create provider.
|
||||
if time.Now().Sub(p.creationTime) > 3*time.Second {
|
||||
if err := p.create(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -57,7 +51,7 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
if p.instance.Load() == nil {
|
||||
if err := p.reload(); err != nil {
|
||||
if err := p.reloadSetting(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.create(); err != nil {
|
||||
@ -65,7 +59,7 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
if err := p.reload(); err != nil {
|
||||
if err := p.reloadSetting(); err != nil {
|
||||
log.Warningf("Failed to refresh configuration, error: %v", err)
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
@ -73,10 +67,11 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return p.instance.Load().(*gooidc.Provider), nil
|
||||
}
|
||||
|
||||
func (p *providerHelper) reload() error {
|
||||
func (p *providerHelper) reloadSetting() error {
|
||||
conf, err := config.OIDCSetting()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load OIDC setting: %v", err)
|
||||
@ -96,10 +91,7 @@ func (p *providerHelper) create() error {
|
||||
return fmt.Errorf("failed to create OIDC provider, error: %v", err)
|
||||
}
|
||||
p.instance.Store(provider)
|
||||
p.ep = endpoint{
|
||||
url: s.Endpoint,
|
||||
VerifyCert: s.VerifyCert,
|
||||
}
|
||||
p.creationTime = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -49,21 +49,20 @@ func TestMain(m *testing.M) {
|
||||
func TestHelperLoadConf(t *testing.T) {
|
||||
testP := &providerHelper{}
|
||||
assert.Nil(t, testP.setting.Load())
|
||||
err := testP.reload()
|
||||
err := testP.reloadSetting()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test", testP.setting.Load().(models.OIDCSetting).Name)
|
||||
assert.Equal(t, endpoint{}, testP.ep)
|
||||
}
|
||||
|
||||
func TestHelperCreate(t *testing.T) {
|
||||
testP := &providerHelper{}
|
||||
err := testP.reload()
|
||||
err := testP.reloadSetting()
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, testP.instance.Load())
|
||||
err = testP.create()
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, "https://accounts.google.com", testP.ep.url)
|
||||
assert.NotNil(t, testP.instance.Load())
|
||||
assert.True(t, time.Now().Sub(testP.creationTime) < 2*time.Second)
|
||||
}
|
||||
|
||||
func TestHelperGet(t *testing.T) {
|
||||
|
114
src/common/utils/redis/helper.go
Normal file
114
src/common/utils/redis/helper.go
Normal file
@ -0,0 +1,114 @@
|
||||
// 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 redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnLock ...
|
||||
ErrUnLock = errors.New("error to release the redis lock")
|
||||
)
|
||||
|
||||
const (
|
||||
unlockScript = `
|
||||
if redis.call("get",KEYS[1]) == ARGV[1] then
|
||||
return redis.call("del",KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`
|
||||
defaultDelay = 5 * time.Second
|
||||
defaultMaxRetry = 5
|
||||
defaultExpiry = 600 * time.Second
|
||||
)
|
||||
|
||||
// Mutex ...
|
||||
type Mutex struct {
|
||||
Conn redis.Conn
|
||||
key string
|
||||
value string
|
||||
opts Options
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(conn redis.Conn, key, value string) *Mutex {
|
||||
o := *DefaultOptions()
|
||||
if value == "" {
|
||||
value = utils.GenerateRandomString()
|
||||
}
|
||||
return &Mutex{conn, key, value, o}
|
||||
}
|
||||
|
||||
// Require retry to require the lock
|
||||
func (rm *Mutex) Require() (bool, error) {
|
||||
var isRequired bool
|
||||
var err error
|
||||
|
||||
for i := 0; i < rm.opts.maxRetry; i++ {
|
||||
isRequired, err = rm.require()
|
||||
if isRequired {
|
||||
break
|
||||
}
|
||||
if err != nil || !isRequired {
|
||||
time.Sleep(rm.opts.retryDelay)
|
||||
}
|
||||
}
|
||||
|
||||
return isRequired, err
|
||||
}
|
||||
|
||||
// require get the redis lock, for details, just refer to https://redis.io/topics/distlock
|
||||
func (rm *Mutex) require() (bool, error) {
|
||||
reply, err := redis.String(rm.Conn.Do("SET", rm.key, rm.value, "NX", "PX", int(rm.opts.expiry/time.Millisecond)))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return reply == "OK", nil
|
||||
}
|
||||
|
||||
// Free releases the lock, for details, just refer to https://redis.io/topics/distlock
|
||||
func (rm *Mutex) Free() (bool, error) {
|
||||
script := redis.NewScript(1, unlockScript)
|
||||
resp, err := redis.Int(script.Do(rm.Conn, rm.key, rm.value))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp == 0 {
|
||||
return false, ErrUnLock
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Options ...
|
||||
type Options struct {
|
||||
retryDelay time.Duration
|
||||
expiry time.Duration
|
||||
maxRetry int
|
||||
}
|
||||
|
||||
// DefaultOptions ...
|
||||
func DefaultOptions() *Options {
|
||||
opt := &Options{
|
||||
retryDelay: defaultDelay,
|
||||
expiry: defaultExpiry,
|
||||
maxRetry: defaultMaxRetry,
|
||||
}
|
||||
return opt
|
||||
}
|
62
src/common/utils/redis/helper_test.go
Normal file
62
src/common/utils/redis/helper_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
// 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 redis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testingRedisHost = "REDIS_HOST"
|
||||
|
||||
func TestRedisLock(t *testing.T) {
|
||||
con, err := redis.Dial(
|
||||
"tcp",
|
||||
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||
redis.DialConnectTimeout(30*time.Second),
|
||||
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||
redis.DialWriteTimeout(10*time.Second),
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
defer con.Close()
|
||||
|
||||
rm := New(con, "test-redis-lock", "test-value")
|
||||
|
||||
successLock, err := rm.Require()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, successLock)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
_, err = rm.Require()
|
||||
assert.NotNil(t, err)
|
||||
|
||||
successUnLock, err := rm.Free()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, successUnLock)
|
||||
|
||||
}
|
||||
|
||||
func getRedisHost() string {
|
||||
redisHost := os.Getenv(testingRedisHost)
|
||||
if redisHost == "" {
|
||||
redisHost = "127.0.0.1" // for local test
|
||||
}
|
||||
|
||||
return redisHost
|
||||
}
|
@ -22,8 +22,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
// "time"
|
||||
|
||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
)
|
||||
@ -130,9 +128,18 @@ func (r *Registry) Catalog() ([]string, error) {
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// Ping ...
|
||||
// Ping checks by Head method
|
||||
func (r *Registry) Ping() error {
|
||||
req, err := http.NewRequest(http.MethodHead, buildPingURL(r.Endpoint.String()), nil)
|
||||
return r.ping(http.MethodHead)
|
||||
}
|
||||
|
||||
// PingGet checks by Get method
|
||||
func (r *Registry) PingGet() error {
|
||||
return r.ping(http.MethodGet)
|
||||
}
|
||||
|
||||
func (r *Registry) ping(method string) error {
|
||||
req, err := http.NewRequest(method, buildPingURL(r.Endpoint.String()), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) (
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusCreated {
|
||||
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
|
||||
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
||||
return
|
||||
}
|
||||
|
@ -144,6 +144,7 @@ func init() {
|
||||
beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog")
|
||||
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/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")
|
||||
|
@ -212,10 +212,10 @@ func jobserviceHealthChecker() health.Checker {
|
||||
}
|
||||
|
||||
func registryHealthChecker() health.Checker {
|
||||
url := getRegistryURL() + "/v2"
|
||||
url := getRegistryURL() + "/"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusUnauthorized)
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
|
@ -158,6 +158,7 @@ func (p *ProjectAPI) Post() {
|
||||
if _, ok := pro.Metadata[models.ProMetaPublic]; !ok {
|
||||
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false)
|
||||
}
|
||||
// populate
|
||||
|
||||
owner := p.SecurityCtx.GetUsername()
|
||||
// set the owner as the system admin when the API being called by replication
|
||||
@ -460,7 +461,8 @@ func (p *ProjectAPI) Put() {
|
||||
|
||||
if err := p.ProjectMgr.Update(p.project.ProjectID,
|
||||
&models.Project{
|
||||
Metadata: req.Metadata,
|
||||
Metadata: req.Metadata,
|
||||
CVEWhitelist: req.CVEWhitelist,
|
||||
}); err != nil {
|
||||
p.ParseAndHandleError(fmt.Sprintf("failed to update project %d",
|
||||
p.project.ProjectID), err)
|
||||
|
@ -52,6 +52,15 @@ func TestProjectMemberAPI_Get(t *testing.T) {
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("/api/projects/1/members/%d", projAdminPMID),
|
||||
credential: admin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
// 404
|
||||
{
|
||||
request: &testingRequest{
|
||||
|
@ -49,6 +49,7 @@ func (t *RegistryAPI) Ping() {
|
||||
ID *int64 `json:"id"`
|
||||
Type *string `json:"type"`
|
||||
URL *string `json:"url"`
|
||||
Region *string `json:"region"`
|
||||
CredentialType *string `json:"credential_type"`
|
||||
AccessKey *string `json:"access_key"`
|
||||
AccessSecret *string `json:"access_secret"`
|
||||
|
@ -17,6 +17,7 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/pkg/scan"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
@ -34,7 +35,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/clair"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/notary"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||
@ -332,7 +332,7 @@ func (ra *RepositoryAPI) Delete() {
|
||||
|
||||
go func(tag string) {
|
||||
e := &event.Event{
|
||||
Type: event.EventTypeImagePush,
|
||||
Type: event.EventTypeImageDelete,
|
||||
Resource: &model.Resource{
|
||||
Type: model.ResourceTypeImage,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
@ -1036,21 +1036,9 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
|
||||
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
res := []*models.VulnerabilityItem{}
|
||||
overview, err := dao.GetImgScanOverview(digest)
|
||||
res, err := scan.VulnListByDigest(digest)
|
||||
if err != nil {
|
||||
ra.SendInternalServerError(fmt.Errorf("failed to get the scan overview, error: %v", err))
|
||||
return
|
||||
}
|
||||
if overview != nil && len(overview.DetailsKey) > 0 {
|
||||
clairClient := clair.NewClient(config.ClairEndpoint(), nil)
|
||||
log.Debugf("The key for getting details: %s", overview.DetailsKey)
|
||||
details, err := clairClient.GetResult(overview.DetailsKey)
|
||||
if err != nil {
|
||||
ra.SendInternalServerError(fmt.Errorf("Failed to get scan details from Clair, error: %v", err))
|
||||
return
|
||||
}
|
||||
res = transformVulnerabilities(details)
|
||||
log.Errorf("Failed to get vulnerability list for image: %s:%s", repository, tag)
|
||||
}
|
||||
ra.Data["json"] = res
|
||||
ra.ServeJSON()
|
||||
|
81
src/core/api/sys_cve_whitelist.go
Normal file
81
src/core/api/sys_cve_whitelist.go
Normal file
@ -0,0 +1,81 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SysCVEWhitelistAPI Handles the requests to manage system level CVE whitelist
|
||||
type SysCVEWhitelistAPI struct {
|
||||
BaseController
|
||||
manager whitelist.Manager
|
||||
}
|
||||
|
||||
// Prepare validates the request initially
|
||||
func (sca *SysCVEWhitelistAPI) Prepare() {
|
||||
sca.BaseController.Prepare()
|
||||
if !sca.SecurityCtx.IsAuthenticated() {
|
||||
sca.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||
return
|
||||
}
|
||||
if !sca.SecurityCtx.IsSysAdmin() && sca.Ctx.Request.Method != http.MethodGet {
|
||||
msg := fmt.Sprintf("only system admin has permission issue %s request to this API", sca.Ctx.Request.Method)
|
||||
log.Errorf(msg)
|
||||
sca.SendForbiddenError(errors.New(msg))
|
||||
return
|
||||
}
|
||||
sca.manager = whitelist.NewDefaultManager()
|
||||
}
|
||||
|
||||
// Get handles the GET request to retrieve the system level CVE whitelist
|
||||
func (sca *SysCVEWhitelistAPI) Get() {
|
||||
l, err := sca.manager.GetSys()
|
||||
if err != nil {
|
||||
sca.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
sca.WriteJSONData(l)
|
||||
}
|
||||
|
||||
// Put handles the PUT request to update the system level CVE whitelist
|
||||
func (sca *SysCVEWhitelistAPI) Put() {
|
||||
var l models.CVEWhitelist
|
||||
if err := sca.DecodeJSONReq(&l); err != nil {
|
||||
log.Errorf("Failed to decode JSON array from request")
|
||||
sca.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
if l.ProjectID != 0 {
|
||||
msg := fmt.Sprintf("Non-zero project ID for system CVE whitelist: %d.", l.ProjectID)
|
||||
log.Error(msg)
|
||||
sca.SendBadRequestError(errors.New(msg))
|
||||
return
|
||||
}
|
||||
if err := sca.manager.SetSys(l); err != nil {
|
||||
if whitelist.IsInvalidErr(err) {
|
||||
log.Errorf("Invalid CVE whitelist: %v", err)
|
||||
sca.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
sca.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
126
src/core/api/sys_cve_whitelist_test.go
Normal file
126
src/core/api/sys_cve_whitelist_test.go
Normal file
@ -0,0 +1,126 @@
|
||||
// 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/models"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSysCVEWhitelistAPIGet(t *testing.T) {
|
||||
url := "/api/system/CVEWhitelist"
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: url,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: url,
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestSysCVEWhitelistAPIPut(t *testing.T) {
|
||||
url := "/api/system/CVEWhitelist"
|
||||
s := int64(1573254000)
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: url,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: url,
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: url,
|
||||
bodyJSON: []string{"CVE-1234-1234"},
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: url,
|
||||
bodyJSON: models.CVEWhitelist{
|
||||
ExpiresAt: &s,
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2019-12310"},
|
||||
},
|
||||
ProjectID: 2,
|
||||
},
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: url,
|
||||
bodyJSON: models.CVEWhitelist{
|
||||
ExpiresAt: &s,
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2019-12310"},
|
||||
{CVEID: "CVE-2019-12310"},
|
||||
},
|
||||
},
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: url,
|
||||
bodyJSON: models.CVEWhitelist{
|
||||
ExpiresAt: &s,
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2019-12310"},
|
||||
},
|
||||
},
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
@ -24,7 +24,6 @@ import (
|
||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/clair"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
||||
@ -279,35 +278,3 @@ func repositoryExist(name string, client *registry.Repository) (bool, error) {
|
||||
}
|
||||
return len(tags) != 0, nil
|
||||
}
|
||||
|
||||
// transformVulnerabilities transforms the returned value of Clair API to a list of VulnerabilityItem
|
||||
func transformVulnerabilities(layerWithVuln *models.ClairLayerEnvelope) []*models.VulnerabilityItem {
|
||||
res := []*models.VulnerabilityItem{}
|
||||
l := layerWithVuln.Layer
|
||||
if l == nil {
|
||||
return res
|
||||
}
|
||||
features := l.Features
|
||||
if features == nil {
|
||||
return res
|
||||
}
|
||||
for _, f := range features {
|
||||
vulnerabilities := f.Vulnerabilities
|
||||
if vulnerabilities == nil {
|
||||
continue
|
||||
}
|
||||
for _, v := range vulnerabilities {
|
||||
vItem := &models.VulnerabilityItem{
|
||||
ID: v.Name,
|
||||
Pkg: f.Name,
|
||||
Version: f.Version,
|
||||
Severity: clair.ParseClairSev(v.Severity),
|
||||
Fixed: v.FixedBy,
|
||||
Link: v.Link,
|
||||
Description: v.Description,
|
||||
}
|
||||
res = append(res, vItem)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
@ -20,11 +20,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
goldap "gopkg.in/ldap.v2"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
ldapUtils "github.com/goharbor/harbor/src/common/utils/ldap"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
@ -79,7 +79,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
u.Username = ldapUsers[0].Username
|
||||
u.Email = strings.TrimSpace(ldapUsers[0].Email)
|
||||
u.Realname = ldapUsers[0].Realname
|
||||
userGroups := make([]*models.UserGroup, 0)
|
||||
ugIDs := []int{}
|
||||
|
||||
dn := ldapUsers[0].DN
|
||||
if err = ldapSession.Bind(dn, m.Password); err != nil {
|
||||
@ -95,6 +95,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
for _, groupDN := range ldapUsers[0].GroupDNList {
|
||||
|
||||
groupDN = utils.TrimLower(groupDN)
|
||||
// Attach LDAP group admin
|
||||
if len(groupAdminDN) > 0 && groupAdminDN == groupDN {
|
||||
u.HasAdminRole = true
|
||||
}
|
||||
@ -103,16 +104,16 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
GroupType: 1,
|
||||
LdapGroupDN: groupDN,
|
||||
}
|
||||
userGroupList, err := group.QueryUserGroup(userGroupQuery)
|
||||
userGroups, err := group.QueryUserGroup(userGroupQuery)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(userGroupList) == 0 {
|
||||
if len(userGroups) == 0 {
|
||||
continue
|
||||
}
|
||||
userGroups = append(userGroups, userGroupList[0])
|
||||
ugIDs = append(ugIDs, userGroups[0].ID)
|
||||
}
|
||||
u.GroupList = userGroups
|
||||
u.GroupIDs = ugIDs
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
@ -229,8 +229,10 @@ type oidcCliReqCtxModifier struct{}
|
||||
|
||||
func (oc *oidcCliReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
path := ctx.Request.URL.Path
|
||||
if path != "/service/token" && !strings.HasPrefix(path, "/chartrepo/") {
|
||||
log.Debug("OIDC CLI modifer only handles request by docker CLI or helm CLI")
|
||||
if path != "/service/token" &&
|
||||
!strings.HasPrefix(path, "/chartrepo/") &&
|
||||
!strings.HasPrefix(path, "/api/chartrepo/") {
|
||||
log.Debug("OIDC CLI modifier only handles request by docker CLI or helm CLI")
|
||||
return false
|
||||
}
|
||||
if ctx.Request.Context().Value(AuthModeKey).(string) != common.OIDCAuth {
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
errutil "github.com/goharbor/harbor/src/common/utils/error"
|
||||
@ -132,19 +131,16 @@ func (d *driver) Update(projectIDOrName interface{},
|
||||
func (d *driver) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
|
||||
var total int64
|
||||
var projects []*models.Project
|
||||
var groupDNCondition string
|
||||
|
||||
// List with LDAP group projects
|
||||
var groupIDs []int
|
||||
if query != nil && query.Member != nil {
|
||||
groupDNCondition = group.GetGroupDNQueryCondition(query.Member.GroupList)
|
||||
groupIDs = query.Member.GroupIDs
|
||||
}
|
||||
|
||||
count, err := dao.GetTotalGroupProjects(groupDNCondition, query)
|
||||
count, err := dao.GetTotalGroupProjects(groupIDs, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total = int64(count)
|
||||
projects, err = dao.GetGroupProjects(groupDNCondition, query)
|
||||
projects, err = dao.GetGroupProjects(groupIDs, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ package promgr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
@ -44,6 +45,7 @@ type defaultProjectManager struct {
|
||||
pmsDriver pmsdriver.PMSDriver
|
||||
metaMgrEnabled bool // if metaMgrEnabled is enabled, metaMgr will be used to CURD metadata
|
||||
metaMgr metamgr.ProjectMetadataManager
|
||||
whitelistMgr whitelist.Manager
|
||||
}
|
||||
|
||||
// NewDefaultProjectManager returns an instance of defaultProjectManager,
|
||||
@ -56,6 +58,7 @@ func NewDefaultProjectManager(driver pmsdriver.PMSDriver, metaMgrEnabled bool) P
|
||||
}
|
||||
if metaMgrEnabled {
|
||||
mgr.metaMgr = metamgr.NewDefaultProjectMetadataManager()
|
||||
mgr.whitelistMgr = whitelist.NewDefaultManager()
|
||||
}
|
||||
return mgr
|
||||
}
|
||||
@ -77,6 +80,11 @@ func (d *defaultProjectManager) Get(projectIDOrName interface{}) (*models.Projec
|
||||
for k, v := range meta {
|
||||
project.Metadata[k] = v
|
||||
}
|
||||
wl, err := d.whitelistMgr.Get(project.ProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
project.CVEWhitelist = *wl
|
||||
}
|
||||
return project, nil
|
||||
}
|
||||
@ -85,9 +93,12 @@ func (d *defaultProjectManager) Create(project *models.Project) (int64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(project.Metadata) > 0 && d.metaMgrEnabled {
|
||||
if err = d.metaMgr.Add(id, project.Metadata); err != nil {
|
||||
log.Errorf("failed to add metadata for project %s: %v", project.Name, err)
|
||||
if d.metaMgrEnabled {
|
||||
d.whitelistMgr.CreateEmpty(project.ProjectID)
|
||||
if len(project.Metadata) > 0 {
|
||||
if err = d.metaMgr.Add(id, project.Metadata); err != nil {
|
||||
log.Errorf("failed to add metadata for project %s: %v", project.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
@ -110,37 +121,40 @@ func (d *defaultProjectManager) Delete(projectIDOrName interface{}) error {
|
||||
}
|
||||
|
||||
func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *models.Project) error {
|
||||
if len(project.Metadata) > 0 && d.metaMgrEnabled {
|
||||
pro, err := d.Get(projectIDOrName)
|
||||
if err != nil {
|
||||
pro, err := d.Get(projectIDOrName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pro == nil {
|
||||
return fmt.Errorf("project %v not found", projectIDOrName)
|
||||
}
|
||||
// TODO transaction?
|
||||
if d.metaMgrEnabled {
|
||||
if err := d.whitelistMgr.Set(pro.ProjectID, project.CVEWhitelist); err != nil {
|
||||
return err
|
||||
}
|
||||
if pro == nil {
|
||||
return fmt.Errorf("project %v not found", projectIDOrName)
|
||||
}
|
||||
|
||||
// TODO transaction?
|
||||
metaNeedUpdated := map[string]string{}
|
||||
metaNeedCreated := map[string]string{}
|
||||
if pro.Metadata == nil {
|
||||
pro.Metadata = map[string]string{}
|
||||
}
|
||||
for key, value := range project.Metadata {
|
||||
_, exist := pro.Metadata[key]
|
||||
if exist {
|
||||
metaNeedUpdated[key] = value
|
||||
} else {
|
||||
metaNeedCreated[key] = value
|
||||
if len(project.Metadata) > 0 {
|
||||
metaNeedUpdated := map[string]string{}
|
||||
metaNeedCreated := map[string]string{}
|
||||
if pro.Metadata == nil {
|
||||
pro.Metadata = map[string]string{}
|
||||
}
|
||||
for key, value := range project.Metadata {
|
||||
_, exist := pro.Metadata[key]
|
||||
if exist {
|
||||
metaNeedUpdated[key] = value
|
||||
} else {
|
||||
metaNeedCreated[key] = value
|
||||
}
|
||||
}
|
||||
if err = d.metaMgr.Add(pro.ProjectID, metaNeedCreated); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = d.metaMgr.Update(pro.ProjectID, metaNeedUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = d.metaMgr.Add(pro.ProjectID, metaNeedCreated); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = d.metaMgr.Update(pro.ProjectID, metaNeedUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return d.pmsDriver.Update(projectIDOrName, project)
|
||||
}
|
||||
|
||||
@ -179,6 +193,7 @@ func (d *defaultProjectManager) List(query *models.ProjectQueryParam) (*models.P
|
||||
project.Metadata = meta
|
||||
}
|
||||
}
|
||||
// the whitelist is not populated deliberately
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
@ -166,9 +166,10 @@ func TestPMSPolicyChecker(t *testing.T) {
|
||||
Name: name,
|
||||
OwnerID: 1,
|
||||
Metadata: map[string]string{
|
||||
models.ProMetaEnableContentTrust: "true",
|
||||
models.ProMetaPreventVul: "true",
|
||||
models.ProMetaSeverity: "low",
|
||||
models.ProMetaEnableContentTrust: "true",
|
||||
models.ProMetaPreventVul: "true",
|
||||
models.ProMetaSeverity: "low",
|
||||
models.ProMetaReuseSysCVEWhitelist: "false",
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
@ -180,9 +181,10 @@ func TestPMSPolicyChecker(t *testing.T) {
|
||||
|
||||
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low")
|
||||
assert.True(t, contentTrustFlag)
|
||||
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low")
|
||||
projectVulnerableEnabled, projectVulnerableSeverity, wl := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low")
|
||||
assert.True(t, projectVulnerableEnabled)
|
||||
assert.Equal(t, projectVulnerableSeverity, models.SevLow)
|
||||
assert.Empty(t, wl.Items)
|
||||
}
|
||||
|
||||
func TestMatchNotaryDigest(t *testing.T) {
|
||||
|
@ -2,7 +2,6 @@ package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/clair"
|
||||
@ -11,6 +10,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||
"github.com/goharbor/harbor/src/pkg/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
@ -82,7 +83,7 @@ type policyChecker interface {
|
||||
// contentTrustEnabled returns whether a project has enabled content trust.
|
||||
contentTrustEnabled(name string) bool
|
||||
// vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity.
|
||||
vulnerablePolicy(name string) (bool, models.Severity)
|
||||
vulnerablePolicy(name string) (bool, models.Severity, models.CVEWhitelist)
|
||||
}
|
||||
|
||||
type pmsPolicyChecker struct {
|
||||
@ -97,13 +98,28 @@ func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool {
|
||||
}
|
||||
return project.ContentTrustEnabled()
|
||||
}
|
||||
func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
|
||||
func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity, models.CVEWhitelist) {
|
||||
project, err := pc.pm.Get(name)
|
||||
wl := models.CVEWhitelist{}
|
||||
if err != nil {
|
||||
log.Errorf("Unexpected error when getting the project, error: %v", err)
|
||||
return true, models.SevUnknown
|
||||
return true, models.SevUnknown, wl
|
||||
}
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity())
|
||||
mgr := whitelist.NewDefaultManager()
|
||||
if project.ReuseSysCVEWhitelist() {
|
||||
w, err := mgr.GetSys()
|
||||
if err != nil {
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
|
||||
}
|
||||
wl = *w
|
||||
} else {
|
||||
w, err := mgr.Get(project.ProjectID)
|
||||
if err != nil {
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
|
||||
}
|
||||
wl = *w
|
||||
}
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
|
||||
}
|
||||
|
||||
// newPMSPolicyChecker returns an instance of an pmsPolicyChecker
|
||||
@ -298,32 +314,39 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName)
|
||||
projectVulnerableEnabled, projectVulnerableSeverity, wl := getPolicyChecker().vulnerablePolicy(img.projectName)
|
||||
if !projectVulnerableEnabled {
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
overview, err := dao.GetImgScanOverview(img.digest)
|
||||
vl, err := scan.VulnListByDigest(img.digest)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get ImgScanOverview with repo: %s, reference: %s, digest: %s. Error: %v", img.repository, img.reference, img.digest, err)
|
||||
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get ImgScanOverview."), http.StatusPreconditionFailed)
|
||||
log.Errorf("Failed to get the vulnerability list, error: %v", err)
|
||||
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get vulnerabilities."), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
// severity is 0 means that the image fails to scan or not scanned successfully.
|
||||
if overview == nil || overview.Sev == 0 {
|
||||
log.Debugf("cannot get the image scan overview info, failing the response.")
|
||||
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Cannot get the image severity."), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
imageSev := overview.Sev
|
||||
if imageSev >= int(projectVulnerableSeverity) {
|
||||
log.Debugf("the image severity: %q is higher then project setting: %q, failing the response.", models.Severity(imageSev), projectVulnerableSeverity)
|
||||
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("The severity of vulnerability of the image: %q is equal or higher than the threshold in project setting: %q.", models.Severity(imageSev), projectVulnerableSeverity)), http.StatusPreconditionFailed)
|
||||
filtered := vl.ApplyWhitelist(wl)
|
||||
msg := vh.filterMsg(img, filtered)
|
||||
log.Info(msg)
|
||||
if int(vl.Severity()) >= int(projectVulnerableSeverity) {
|
||||
log.Debugf("the image severity: %q is higher then project setting: %q, failing the response.", vl.Severity(), projectVulnerableSeverity)
|
||||
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("The severity of vulnerability of the image: %q is equal or higher than the threshold in project setting: %q.", vl.Severity(), projectVulnerableSeverity)), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (vh vulnerableHandler) filterMsg(img imageInfo, filtered scan.VulnerabilityList) string {
|
||||
filterMsg := fmt.Sprintf("Image: %s/%s:%s, digest: %s, vulnerabilities fitered by whitelist:", img.projectName, img.repository, img.reference, img.digest)
|
||||
if len(filtered) == 0 {
|
||||
filterMsg = fmt.Sprintf("%s none.", filterMsg)
|
||||
}
|
||||
for _, v := range filtered {
|
||||
filterMsg = fmt.Sprintf("%s ID: %s, severity: %s;", filterMsg, v.ID, v.Severity)
|
||||
}
|
||||
return filterMsg
|
||||
}
|
||||
|
||||
func matchNotaryDigest(img imageInfo) (bool, error) {
|
||||
if NotaryEndpoint == "" {
|
||||
NotaryEndpoint = config.InternalNotaryEndpoint()
|
||||
|
@ -38,7 +38,15 @@ func Init(urls ...string) error {
|
||||
return err
|
||||
}
|
||||
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||
handlers = handlerChain{head: readonlyHandler{next: urlHandler{next: multipleManifestHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}}}}
|
||||
handlers = handlerChain{
|
||||
head: readonlyHandler{
|
||||
next: urlHandler{
|
||||
next: multipleManifestHandler{
|
||||
next: listReposHandler{
|
||||
next: contentTrustHandler{
|
||||
next: vulnerableHandler{
|
||||
next: Proxy,
|
||||
}}}}}}}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,7 @@ func initRouters() {
|
||||
beego.Router("/api/system/gc/:id([0-9]+)/log", &api.GCAPI{}, "get:GetLog")
|
||||
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/logs", &api.LogAPI{})
|
||||
|
||||
|
@ -10,6 +10,7 @@ require (
|
||||
github.com/Unknwon/goconfig v0.0.0-20160216183935-5f601ca6ef4d // indirect
|
||||
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
|
||||
github.com/astaxie/beego v1.9.0
|
||||
github.com/aws/aws-sdk-go v1.19.47
|
||||
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
|
@ -25,6 +25,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/astaxie/beego v1.9.0 h1:tPzS+D1oCLi+SEb/TLNRNYpCjaMVfAGoy9OTLwS5ul4=
|
||||
github.com/astaxie/beego v1.9.0/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U=
|
||||
github.com/aws/aws-sdk-go v1.19.47 h1:ZEze0mpk8Fttrsz6UNLqhH/jRGYbMPfWFA2ILas4AmM=
|
||||
github.com/aws/aws-sdk-go v1.19.47/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0 h1:fQaDnUQvBXHHQdGBu9hz8nPznB4BeiPQokvmQVjmNEw=
|
||||
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0/go.mod h1:KLeFCpAMq2+50NkXC8iiJxLLiiTfTqrGtKEVm+2fk7s=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@ -158,6 +160,8 @@ github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYX
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.0 h1:6WV8LvwPpDhKjo5U9O6b4+xdG/jTXNPwlDme/MTo8Ns=
|
||||
github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
|
@ -34,7 +34,7 @@ func RedisKeyScheduled(namespace string) string {
|
||||
|
||||
// RedisKeyLastPeriodicEnqueue returns key of timestamp if last periodic enqueue.
|
||||
func RedisKeyLastPeriodicEnqueue(namespace string) string {
|
||||
return RedisNamespacePrefix(namespace) + "last_periodic_enqueue"
|
||||
return RedisNamespacePrefix(namespace) + "last_periodic_enqueue_h"
|
||||
}
|
||||
|
||||
// KeyNamespacePrefix returns the based key based on the namespace.
|
||||
|
@ -34,6 +34,12 @@ import (
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/native"
|
||||
// register the Huawei adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/huawei"
|
||||
// register the Google Gcr adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/googlegcr"
|
||||
// register the AwsEcr adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/awsecr"
|
||||
// register the AzureAcr adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/azurecr"
|
||||
)
|
||||
|
||||
// Replication implements the job interface
|
||||
|
@ -16,6 +16,12 @@ package period
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/common/rds"
|
||||
"github.com/goharbor/harbor/src/jobservice/common/utils"
|
||||
"github.com/goharbor/harbor/src/jobservice/env"
|
||||
@ -26,9 +32,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EnqueuerTestSuite tests functions of enqueuer
|
||||
@ -89,19 +92,30 @@ func (suite *EnqueuerTestSuite) TestEnqueuer() {
|
||||
suite.enqueuer.stopChan <- true
|
||||
}()
|
||||
|
||||
<-time.After(1 * time.Second)
|
||||
|
||||
key := rds.RedisKeyScheduled(suite.namespace)
|
||||
conn := suite.pool.Get()
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
count, err := redis.Int(conn.Do("ZCARD", key))
|
||||
require.Nil(suite.T(), err, "count scheduled: nil error expected but got %s", err)
|
||||
assert.Condition(suite.T(), func() bool {
|
||||
return count > 0
|
||||
}, "count of scheduled jobs should be greater than 0 but got %d", count)
|
||||
tk := time.NewTicker(500 * time.Millisecond)
|
||||
defer tk.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tk.C:
|
||||
count, err := redis.Int(conn.Do("ZCARD", key))
|
||||
require.Nil(suite.T(), err, "count scheduled: nil error expected but got %s", err)
|
||||
if assert.Condition(suite.T(), func() (success bool) {
|
||||
return count > 0
|
||||
}, "at least one job should be scheduled for the periodic job policy") {
|
||||
return
|
||||
}
|
||||
case <-time.After(15 * time.Second):
|
||||
require.NoError(suite.T(), errors.New("timeout (15s): expect at 1 scheduled job but still get nothing"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := suite.enqueuer.start()
|
||||
@ -112,7 +126,7 @@ func (suite *EnqueuerTestSuite) prepare() {
|
||||
now := time.Now()
|
||||
minute := now.Minute()
|
||||
|
||||
coreSpec := fmt.Sprintf("30,50 %d * * * *", minute+2)
|
||||
coreSpec := fmt.Sprintf("0-59 %d * * * *", minute)
|
||||
|
||||
// Prepare one
|
||||
p := &Policy{
|
||||
|
136
src/pkg/scan/vuln.go
Normal file
136
src/pkg/scan/vuln.go
Normal file
@ -0,0 +1,136 @@
|
||||
// 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 scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/clair"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// VulnerabilityItem represents a vulnerability reported by scanner
|
||||
type VulnerabilityItem struct {
|
||||
ID string `json:"id"`
|
||||
Severity models.Severity `json:"severity"`
|
||||
Pkg string `json:"package"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Link string `json:"link"`
|
||||
Fixed string `json:"fixedVersion,omitempty"`
|
||||
}
|
||||
|
||||
// VulnerabilityList is a list of vulnerabilities, which should be scanner-agnostic
|
||||
type VulnerabilityList []VulnerabilityItem
|
||||
|
||||
// ApplyWhitelist filters out the CVE defined in the whitelist in the parm.
|
||||
// It returns the items that are filtered for the caller to track or log.
|
||||
func (vl *VulnerabilityList) ApplyWhitelist(whitelist models.CVEWhitelist) VulnerabilityList {
|
||||
filtered := VulnerabilityList{}
|
||||
if whitelist.IsExpired() {
|
||||
log.Info("The input whitelist is expired, skip filtering")
|
||||
return filtered
|
||||
}
|
||||
s := whitelist.CVESet()
|
||||
r := (*vl)[:0]
|
||||
for _, v := range *vl {
|
||||
if _, ok := s[v.ID]; ok {
|
||||
log.Debugf("Filtered Vulnerability in whitelist, CVE ID: %s, severity: %s", v.ID, v.Severity)
|
||||
filtered = append(filtered, v)
|
||||
} else {
|
||||
r = append(r, v)
|
||||
}
|
||||
}
|
||||
val := reflect.ValueOf(vl)
|
||||
val.Elem().SetLen(len(r))
|
||||
return filtered
|
||||
}
|
||||
|
||||
// Severity returns the highest severity of the vulnerabilities in the list
|
||||
func (vl *VulnerabilityList) Severity() models.Severity {
|
||||
s := models.SevNone
|
||||
for _, v := range *vl {
|
||||
if v.Severity > s {
|
||||
s = v.Severity
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// HasCVE returns whether the vulnerability list has the vulnerability with CVE ID in the parm
|
||||
func (vl *VulnerabilityList) HasCVE(id string) bool {
|
||||
for _, v := range *vl {
|
||||
if v.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// VulnListFromClairResult transforms the returned value of Clair API to a VulnerabilityList
|
||||
func VulnListFromClairResult(layerWithVuln *models.ClairLayerEnvelope) VulnerabilityList {
|
||||
res := VulnerabilityList{}
|
||||
if layerWithVuln == nil {
|
||||
return res
|
||||
}
|
||||
l := layerWithVuln.Layer
|
||||
if l == nil {
|
||||
return res
|
||||
}
|
||||
features := l.Features
|
||||
if features == nil {
|
||||
return res
|
||||
}
|
||||
for _, f := range features {
|
||||
vulnerabilities := f.Vulnerabilities
|
||||
if vulnerabilities == nil {
|
||||
continue
|
||||
}
|
||||
for _, v := range vulnerabilities {
|
||||
vItem := VulnerabilityItem{
|
||||
ID: v.Name,
|
||||
Pkg: f.Name,
|
||||
Version: f.Version,
|
||||
Severity: clair.ParseClairSev(v.Severity),
|
||||
Fixed: v.FixedBy,
|
||||
Link: v.Link,
|
||||
Description: v.Description,
|
||||
}
|
||||
res = append(res, vItem)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// VulnListByDigest returns the VulnerabilityList based on the scan result of artifact with the digest in the parm
|
||||
func VulnListByDigest(digest string) (VulnerabilityList, error) {
|
||||
var res VulnerabilityList
|
||||
overview, err := dao.GetImgScanOverview(digest)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if overview == nil || len(overview.DetailsKey) == 0 {
|
||||
return res, fmt.Errorf("unable to get the scan result for digest: %s, the artifact is not scanned", digest)
|
||||
}
|
||||
c := clair.NewClient(config.ClairEndpoint(), nil)
|
||||
clairRes, err := c.GetResult(overview.DetailsKey)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("failed to get scan result from Clair, error: %v", err)
|
||||
}
|
||||
return VulnListFromClairResult(clairRes), nil
|
||||
}
|
178
src/pkg/scan/vuln_test.go
Normal file
178
src/pkg/scan/vuln_test.go
Normal file
@ -0,0 +1,178 @@
|
||||
// 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 scan
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
past = int64(1561967574)
|
||||
vulnList1 = VulnerabilityList{}
|
||||
vulnList2 = VulnerabilityList{
|
||||
{ID: "CVE-2018-10754",
|
||||
Severity: models.SevLow,
|
||||
Pkg: "ncurses",
|
||||
Version: "6.0+20161126-1+deb9u2",
|
||||
},
|
||||
{
|
||||
ID: "CVE-2018-6485",
|
||||
Severity: models.SevHigh,
|
||||
Pkg: "glibc",
|
||||
Version: "2.24-11+deb9u4",
|
||||
},
|
||||
}
|
||||
whiteList1 = models.CVEWhitelist{
|
||||
ExpiresAt: &past,
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2018-6485"},
|
||||
},
|
||||
}
|
||||
whiteList2 = models.CVEWhitelist{
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2018-6485"},
|
||||
},
|
||||
}
|
||||
whiteList3 = models.CVEWhitelist{
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2018-6485"},
|
||||
{CVEID: "CVE-2018-10754"},
|
||||
{CVEID: "CVE-2019-12817"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dao.PrepareTestForPostgresSQL()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestVulnerabilityList_HasCVE(t *testing.T) {
|
||||
cases := []struct {
|
||||
input VulnerabilityList
|
||||
cve string
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
input: vulnList1,
|
||||
cve: "CVE-2018-10754",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
input: vulnList2,
|
||||
cve: "CVE-2018-10754",
|
||||
result: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.result, c.input.HasCVE(c.cve))
|
||||
}
|
||||
}
|
||||
|
||||
func TestVulnerabilityList_Severity(t *testing.T) {
|
||||
cases := []struct {
|
||||
input VulnerabilityList
|
||||
expect models.Severity
|
||||
}{
|
||||
{
|
||||
input: vulnList1,
|
||||
expect: models.SevNone,
|
||||
}, {
|
||||
input: vulnList2,
|
||||
expect: models.SevHigh,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expect, c.input.Severity())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVulnerabilityList_ApplyWhitelist(t *testing.T) {
|
||||
cases := []struct {
|
||||
vl VulnerabilityList
|
||||
wl models.CVEWhitelist
|
||||
expectFiltered VulnerabilityList
|
||||
expectSev models.Severity
|
||||
}{
|
||||
{
|
||||
vl: vulnList2,
|
||||
wl: whiteList1,
|
||||
expectFiltered: VulnerabilityList{},
|
||||
expectSev: models.SevHigh,
|
||||
},
|
||||
{
|
||||
vl: vulnList2,
|
||||
wl: whiteList2,
|
||||
expectFiltered: VulnerabilityList{
|
||||
{
|
||||
ID: "CVE-2018-6485",
|
||||
Severity: models.SevHigh,
|
||||
Pkg: "glibc",
|
||||
Version: "2.24-11+deb9u4",
|
||||
},
|
||||
},
|
||||
expectSev: models.SevLow,
|
||||
},
|
||||
{
|
||||
vl: vulnList1,
|
||||
wl: whiteList3,
|
||||
expectFiltered: VulnerabilityList{},
|
||||
expectSev: models.SevNone,
|
||||
},
|
||||
{
|
||||
vl: vulnList2,
|
||||
wl: whiteList3,
|
||||
expectFiltered: VulnerabilityList{
|
||||
{ID: "CVE-2018-10754",
|
||||
Severity: models.SevLow,
|
||||
Pkg: "ncurses",
|
||||
Version: "6.0+20161126-1+deb9u2",
|
||||
},
|
||||
{
|
||||
ID: "CVE-2018-6485",
|
||||
Severity: models.SevHigh,
|
||||
Pkg: "glibc",
|
||||
Version: "2.24-11+deb9u4",
|
||||
},
|
||||
},
|
||||
expectSev: models.SevNone,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
filtered := c.vl.ApplyWhitelist(c.wl)
|
||||
assert.Equal(t, c.expectFiltered, filtered)
|
||||
assert.Equal(t, c.vl.Severity(), c.expectSev)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVulnListByDigest(t *testing.T) {
|
||||
_, err := VulnListByDigest("notexist")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestVulnListFromClairResult(t *testing.T) {
|
||||
l := VulnListFromClairResult(nil)
|
||||
assert.Equal(t, VulnerabilityList{}, l)
|
||||
lv := &models.ClairLayerEnvelope{
|
||||
Layer: nil,
|
||||
Error: nil,
|
||||
}
|
||||
l2 := VulnListFromClairResult(lv)
|
||||
assert.Equal(t, VulnerabilityList{}, l2)
|
||||
}
|
79
src/pkg/scan/whitelist/manager.go
Normal file
79
src/pkg/scan/whitelist/manager.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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 whitelist
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
)
|
||||
|
||||
// Manager defines the interface of CVE whitelist manager, it support both system level and project level whitelists
|
||||
type Manager interface {
|
||||
// CreateEmpty creates empty whitelist for given project
|
||||
CreateEmpty(projectID int64) error
|
||||
// Set sets the whitelist for given project (create or update)
|
||||
Set(projectID int64, list models.CVEWhitelist) error
|
||||
// Get gets the whitelist for given project
|
||||
Get(projectID int64) (*models.CVEWhitelist, error)
|
||||
// SetSys sets system level whitelist
|
||||
SetSys(list models.CVEWhitelist) error
|
||||
// GetSys gets system level whitelist
|
||||
GetSys() (*models.CVEWhitelist, error)
|
||||
}
|
||||
|
||||
type defaultManager struct{}
|
||||
|
||||
// CreateEmpty creates empty whitelist for given project
|
||||
func (d *defaultManager) CreateEmpty(projectID int64) error {
|
||||
l := models.CVEWhitelist{
|
||||
ProjectID: projectID,
|
||||
}
|
||||
_, err := dao.UpdateCVEWhitelist(l)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to create empty CVE whitelist for project: %d, error: %v", projectID, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Set sets the whitelist for given project (create or update)
|
||||
func (d *defaultManager) Set(projectID int64, list models.CVEWhitelist) error {
|
||||
list.ProjectID = projectID
|
||||
if err := Validate(list); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := dao.UpdateCVEWhitelist(list)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get gets the whitelist for given project
|
||||
func (d *defaultManager) Get(projectID int64) (*models.CVEWhitelist, error) {
|
||||
return dao.GetCVEWhitelist(projectID)
|
||||
}
|
||||
|
||||
// SetSys sets the system level whitelist
|
||||
func (d *defaultManager) SetSys(list models.CVEWhitelist) error {
|
||||
return d.Set(0, list)
|
||||
}
|
||||
|
||||
// GetSys gets the system level whitelist
|
||||
func (d *defaultManager) GetSys() (*models.CVEWhitelist, error) {
|
||||
return d.Get(0)
|
||||
}
|
||||
|
||||
// NewDefaultManager return a new instance of defaultManager
|
||||
func NewDefaultManager() Manager {
|
||||
return &defaultManager{}
|
||||
}
|
60
src/pkg/scan/whitelist/validator.go
Normal file
60
src/pkg/scan/whitelist/validator.go
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 whitelist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type invalidErr struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (ie *invalidErr) Error() string {
|
||||
return ie.msg
|
||||
}
|
||||
|
||||
// NewInvalidErr ...
|
||||
func NewInvalidErr(s string) error {
|
||||
return &invalidErr{
|
||||
msg: s,
|
||||
}
|
||||
}
|
||||
|
||||
// IsInvalidErr checks if the error is an invalidErr
|
||||
func IsInvalidErr(err error) bool {
|
||||
_, ok := err.(*invalidErr)
|
||||
return ok
|
||||
}
|
||||
|
||||
const cveIDPattern = `^CVE-\d{4}-\d+$`
|
||||
|
||||
// Validate help validates the CVE whitelist, to ensure the CVE ID is valid and there's no duplication
|
||||
func Validate(wl models.CVEWhitelist) error {
|
||||
m := map[string]struct{}{}
|
||||
re := regexp.MustCompile(cveIDPattern)
|
||||
for _, it := range wl.Items {
|
||||
if !re.MatchString(it.CVEID) {
|
||||
return &invalidErr{fmt.Sprintf("invalid CVE ID: %s", it.CVEID)}
|
||||
}
|
||||
if _, ok := m[it.CVEID]; ok {
|
||||
return &invalidErr{fmt.Sprintf("duplicate CVE ID in whitelist: %s", it.CVEID)}
|
||||
}
|
||||
m[it.CVEID] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
102
src/pkg/scan/whitelist/validator_test.go
Normal file
102
src/pkg/scan/whitelist/validator_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
// 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 whitelist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsInvalidErr(t *testing.T) {
|
||||
cases := []struct {
|
||||
instance error
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
instance: nil,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
instance: fmt.Errorf("whatever"),
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
instance: NewInvalidErr("This is true"),
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for n, c := range cases {
|
||||
t.Logf("Executing TestIsInvalidErr case: %d\n", n)
|
||||
assert.Equal(t, c.expect, IsInvalidErr(c.instance))
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
l models.CVEWhitelist
|
||||
noError bool
|
||||
}{
|
||||
{
|
||||
l: models.CVEWhitelist{
|
||||
Items: nil,
|
||||
},
|
||||
noError: true,
|
||||
},
|
||||
{
|
||||
l: models.CVEWhitelist{
|
||||
Items: []models.CVEWhitelistItem{},
|
||||
},
|
||||
noError: true,
|
||||
},
|
||||
{
|
||||
l: models.CVEWhitelist{
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "breakit"},
|
||||
},
|
||||
},
|
||||
noError: false,
|
||||
},
|
||||
{
|
||||
l: models.CVEWhitelist{
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2014-456132"},
|
||||
{CVEID: "CVE-2014-7654321"},
|
||||
},
|
||||
},
|
||||
noError: true,
|
||||
},
|
||||
{
|
||||
l: models.CVEWhitelist{
|
||||
Items: []models.CVEWhitelistItem{
|
||||
{CVEID: "CVE-2014-456132"},
|
||||
{CVEID: "CVE-2014-456132"},
|
||||
{CVEID: "CVE-2014-7654321"},
|
||||
},
|
||||
},
|
||||
noError: false,
|
||||
},
|
||||
}
|
||||
for n, c := range cases {
|
||||
t.Logf("Executing TestValidate case: %d\n", n)
|
||||
e := Validate(c.l)
|
||||
assert.Equal(t, c.noError, e == nil)
|
||||
if e != nil {
|
||||
assert.True(t, IsInvalidErr(e))
|
||||
}
|
||||
}
|
||||
}
|
@ -44,11 +44,15 @@
|
||||
<div class="form-group">
|
||||
<label for="destination_url" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.URL' |
|
||||
translate }}</label>
|
||||
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)"
|
||||
[class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
|
||||
<input type="text" id="destination_url" [disabled]="testOngoing || urlDisabled" [readonly]="!editable" [(ngModel)]="target.url"
|
||||
size="25" name="endpointUrl" #targetEndpoint="ngModel" required placeholder="http(s)://192.168.1.1">
|
||||
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
|
||||
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint?.errors && (targetEndpoint?.dirty || targetEndpoint?.touched)"
|
||||
[class.valid]="targetEndpoint?.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
|
||||
<input *ngIf="!endpointList.length" type="text" id="destination_url" [disabled]="testOngoing || urlDisabled" [readonly]="!editable" [(ngModel)]="target.url"
|
||||
size="25" name="endpointUrl" #targetEndpoint="ngModel" required placeholder="http(s)://192.168.1.1">
|
||||
<select *ngIf="endpointList.length" [(ngModel)]="target.url" name="endpointUrl" #targetEndpoint="ngModel">
|
||||
<option class="display-none" value=""></option>
|
||||
<option *ngFor="let endpoint of endpointList" value="{{endpoint.value}}">{{endpoint.key}}</option>
|
||||
</select>
|
||||
<span class="tooltip-content" *ngIf="targetEndpoint?.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
|
||||
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
@ -57,15 +61,19 @@
|
||||
<div class="form-group">
|
||||
<label for="destination_access_key" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_ID' |
|
||||
translate }}</label>
|
||||
<input type="text" placeholder="Access ID" class="col-md-8" id="destination_access_key" [disabled]="testOngoing" [readonly]="!editable"
|
||||
<input type="text" placeholder="Access ID" class="col-md-8" id="destination_access_key" [disabled]="testOngoing" [readonly]="target.type ==='google-gcr' || !editable"
|
||||
[(ngModel)]="target.credential.access_key" size="28" name="access_key" #access_key="ngModel">
|
||||
</div>
|
||||
<!-- access_secret -->
|
||||
<div class="form-group">
|
||||
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_SECRET' |
|
||||
translate }}</label>
|
||||
<input type="password" placeholder="Access Secret" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
|
||||
[(ngModel)]="target.credential.access_secret" size="28" name="access_secret" #access_secret="ngModel">
|
||||
<input *ngIf="target.type !=='google-gcr';else gcr_secret" type="password" placeholder="Access Secret" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
|
||||
[(ngModel)]="target.credential.access_secret" size="28" name="access_secret" #access_secret="ngModel">
|
||||
<ng-template #gcr_secret>
|
||||
<textarea type="text" row="3" placeholder="Json Secret" class="inputWidth" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
|
||||
[(ngModel)]="target.credential.access_secret" name="access_secret" #access_secret="ngModel"></textarea>
|
||||
</ng-template>
|
||||
</div>
|
||||
<!-- Verify Remote Cert -->
|
||||
<div class="form-group">
|
||||
@ -88,7 +96,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="inProgress || targetEndpoint.errors">{{
|
||||
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="inProgress || (targetEndpoint?.errors)">{{
|
||||
'DESTINATION.TEST_CONNECTION' | translate }}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' |
|
||||
translate }}</button>
|
||||
|
@ -15,3 +15,6 @@
|
||||
.inputWidth {
|
||||
width: 216px;
|
||||
}
|
||||
.display-none {
|
||||
display: none
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import { Endpoint, PingEndpoint } from "../service/interface";
|
||||
import { clone, compareValue, isEmptyObject } from "../utils";
|
||||
|
||||
const FAKE_PASSWORD = "rjGcfuRu";
|
||||
const FAKE_JSON_KEY = "No Change";
|
||||
const DOCKERHUB_URL = "https://hub.docker.com";
|
||||
@Component({
|
||||
selector: "hbr-create-edit-endpoint",
|
||||
@ -49,12 +50,13 @@ export class CreateEditEndpointComponent
|
||||
closable: boolean = false;
|
||||
editable: boolean;
|
||||
adapterList: string[];
|
||||
endpointList: object[] = [];
|
||||
target: Endpoint = this.initEndpoint();
|
||||
selectedType: string;
|
||||
initVal: Endpoint;
|
||||
targetForm: NgForm;
|
||||
@ViewChild("targetForm") currentForm: NgForm;
|
||||
|
||||
targetEndpoint;
|
||||
testOngoing: boolean;
|
||||
onGoing: boolean;
|
||||
endpointId: number | string;
|
||||
@ -188,8 +190,8 @@ export class CreateEditEndpointComponent
|
||||
this.urlDisabled = this.target.type === 'docker-hub' ? true : false;
|
||||
// Keep data cache
|
||||
this.initVal = clone(target);
|
||||
this.initVal.credential.access_secret = FAKE_PASSWORD;
|
||||
this.target.credential.access_secret = FAKE_PASSWORD;
|
||||
this.initVal.credential.access_secret = this.target.type === 'google-gcr' ? FAKE_JSON_KEY : FAKE_PASSWORD;
|
||||
this.target.credential.access_secret = this.target.type === 'google-gcr' ? FAKE_JSON_KEY : FAKE_PASSWORD;
|
||||
|
||||
// Open the modal now
|
||||
this.open();
|
||||
@ -219,6 +221,104 @@ export class CreateEditEndpointComponent
|
||||
this.urlDisabled = false;
|
||||
this.targetForm.controls.endpointUrl.setValue("");
|
||||
}
|
||||
if (selectValue === 'google-gcr') {
|
||||
this.targetForm.controls.access_key.setValue("_json_key");
|
||||
} else {
|
||||
this.targetForm.controls.access_key.setValue("");
|
||||
}
|
||||
if (selectValue === 'google-gcr') {
|
||||
this.endpointList = [
|
||||
{
|
||||
key: "gcr.io",
|
||||
value: "https://gcr.io"
|
||||
},
|
||||
{
|
||||
key: "us.gcr.io",
|
||||
value: "https://us.gcr.io"
|
||||
},
|
||||
{
|
||||
key: "eu.gcr.io",
|
||||
value: "https://eu.gcr.io"
|
||||
},
|
||||
{
|
||||
key: "asia.gcr.io",
|
||||
value: "https://asia.gcr.io"
|
||||
}
|
||||
];
|
||||
} else if (selectValue === 'aws-ecr') {
|
||||
this.endpointList = [
|
||||
{
|
||||
key: "ap-northeast-1",
|
||||
value: "https://api.ecr.ap-northeast-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "us-east-1",
|
||||
value: "https://api.ecr.us-east-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "us-east-2",
|
||||
value: "https://api.ecr.us-east-2.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "us-west-1",
|
||||
value: "https://api.ecr.us-west-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "us-west-2",
|
||||
value: "https://api.ecr.us-west-2.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "ap-east-1",
|
||||
value: "https://api.ecr.ap-east-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "ap-south-1",
|
||||
value: "https://api.ecr.ap-south-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "ap-northeast-2",
|
||||
value: "https://api.ecr.ap-northeast-2.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "ap-southeast-1",
|
||||
value: "https://api.ecr.ap-southeast-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "ap-southeast-2",
|
||||
value: "https://api.ecr.ap-southeast-2.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "ca-central-1",
|
||||
value: "https://api.ecr.ca-central-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "eu-central-1",
|
||||
value: "https://api.ecr.eu-central-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "eu-west-1",
|
||||
value: "https://api.ecr.eu-west-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "eu-west-2",
|
||||
value: "https://api.ecr.eu-west-2.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "eu-west-3",
|
||||
value: "https://api.ecr.eu-west-3.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "eu-north-1",
|
||||
value: "https://api.ecr.eu-north-1.amazonaws.com"
|
||||
},
|
||||
{
|
||||
key: "sa-east-1",
|
||||
value: "https://api.ecr.sa-east-1.amazonaws.com"
|
||||
}
|
||||
];
|
||||
} else {
|
||||
this.endpointList = [];
|
||||
}
|
||||
}
|
||||
|
||||
testConnection() {
|
||||
|
@ -62,8 +62,8 @@
|
||||
<span class="spinner spinner-inline spinner-position" [hidden]="onGoing === false"></span>
|
||||
<div formArrayName="filters">
|
||||
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
|
||||
<div [formGroupName]="i">
|
||||
<div class="width-70">
|
||||
<div [formGroupName]="i" *ngIf="supportedFilters[i]?.type !=='label' || (supportedFilters[i]?.type==='label' && supportedFilterLabels?.length)">
|
||||
<div class="width-70" >
|
||||
<label>{{"REPLICATION." + supportedFilters[i]?.type.toUpperCase() | translate}}:</label>
|
||||
</div>
|
||||
<label *ngIf="supportedFilters[i]?.style==='input'" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
|
||||
@ -77,6 +77,27 @@
|
||||
<option *ngFor="let value of supportedFilters[i]?.values;" value="{{value}}">{{value}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="select resource-box" *ngIf="supportedFilters[i]?.type==='label'&& supportedFilters[i]?.style==='list'">
|
||||
<div class="dropdown width-100" formArrayName="value">
|
||||
<clr-dropdown class="width-100">
|
||||
<button type="button" class="width-100 dropdown-toggle btn btn-link statistic-data label-text" clrDropdownTrigger>
|
||||
<ng-template ngFor let-label [ngForOf]="filter.value.value" let-m="index">
|
||||
<span class="label" *ngIf="m<1"> {{label}} </span>
|
||||
</ng-template>
|
||||
<span class="ellipsis" *ngIf="filter.value.value.length>1">···</span>
|
||||
<div *ngFor="let label1 of filter.value.value;let k = index" hidden="true">
|
||||
<input type="text" [formControlName]="k" #labelValue id="{{'label_'+ supportedFilters[i]?.type + '_' + label1}}" name="{{'label_'+ supportedFilters[i]?.type + '_' + label1}}" placeholder="select labels" >
|
||||
</div>
|
||||
</button>
|
||||
<clr-dropdown-menu class="width-100" clrPosition="bottom-left" *clrIfOpen>
|
||||
<button type="button" class="dropdown-item" *ngFor="let value of supportedFilterLabels" (click)="stickLabel(value,i)">
|
||||
<clr-icon shape="check" [hidden]="!value.select" class='pull-left'></clr-icon>
|
||||
<div class='labelDiv'><hbr-label-piece [label]="value" [labelWidth]="130"></hbr-label-piece></div>
|
||||
</button>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-box" *ngIf="supportedFilters[i]?.style==='radio' && supportedFilters[i]?.values.length <= 1">
|
||||
<span>{{supportedFilters[i]?.values}}</span>
|
||||
</div>
|
||||
@ -85,7 +106,8 @@
|
||||
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
|
||||
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='name'">{{'TOOLTIP.NAME_FILTER' | translate}}</span>
|
||||
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='tag'">{{'TOOLTIP.TAG_FILTER' | translate}}</span>
|
||||
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='resource'">{{'TOOLTIP.RESOURCE_FILTER' | translate}}</span>
|
||||
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='label'">{{'TOOLTIP.LABEL_FILTER' | translate}}</span>
|
||||
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='resource'">{{'TOOLTIP.RESOURCE_FILTER' | translate}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</div>
|
||||
|
@ -268,4 +268,29 @@ clr-modal {
|
||||
|
||||
.display-none{
|
||||
display: none;
|
||||
}
|
||||
.width-100 {
|
||||
width: 100%;
|
||||
}
|
||||
.label-text {
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #000;
|
||||
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);
|
||||
}
|
||||
.labelDiv {
|
||||
padding-left: 26px;
|
||||
}
|
||||
.ellipsis {
|
||||
margin-left: 0.2rem;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
@ -32,6 +32,7 @@ import { ErrorHandler } from "../error-handler/error-handler";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { EndpointService } from "../service/endpoint.service";
|
||||
import { cronRegex } from "../utils";
|
||||
import { FilterType } from "../shared/shared.const";
|
||||
|
||||
|
||||
@Component({
|
||||
@ -67,6 +68,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
cronString: string;
|
||||
supportedTriggers: string[];
|
||||
supportedFilters: Filter[];
|
||||
supportedFilterLabels: { name: string; color: string; select: boolean; scope: string; }[] = [];
|
||||
|
||||
@Input() withAdmiral: boolean;
|
||||
|
||||
@Output() goToRegistry = new EventEmitter<any>();
|
||||
@ -92,6 +95,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
this.supportedFilters = adapter.supported_resource_filters;
|
||||
this.supportedFilters.forEach(element => {
|
||||
this.filters.push(this.initFilter(element.type));
|
||||
// get supportedFilterLabels labels from supportedFilters
|
||||
this.getLabelListFromAdapter(element);
|
||||
});
|
||||
this.supportedTriggers = adapter.supported_triggers;
|
||||
this.ruleForm.get("trigger").get("type").setValue(this.supportedTriggers[0]);
|
||||
@ -264,12 +269,29 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
return this.ruleForm.get("filters") as FormArray;
|
||||
}
|
||||
setFilter(filters: Filter[]) {
|
||||
const filterFGs = filters.map(filter => this.fb.group(filter));
|
||||
const filterFGs = filters.map(filter => {
|
||||
if (filter.type === FilterType.LABEL) {
|
||||
let fbLabel = this.fb.group({
|
||||
type: FilterType.LABEL
|
||||
});
|
||||
let filterLabel = this.fb.array(filter.value);
|
||||
fbLabel.setControl('value', filterLabel);
|
||||
return fbLabel;
|
||||
} else {
|
||||
return this.fb.group(filter);
|
||||
}
|
||||
});
|
||||
const filterFormArray = this.fb.array(filterFGs);
|
||||
this.ruleForm.setControl("filters", filterFormArray);
|
||||
}
|
||||
|
||||
initFilter(name: string) {
|
||||
if (name === FilterType.LABEL) {
|
||||
const labelArray = this.fb.array([]);
|
||||
const labelControl = this.fb.group({type: name});
|
||||
labelControl.setControl('value', labelArray);
|
||||
return labelControl;
|
||||
}
|
||||
return this.fb.group({
|
||||
type: name,
|
||||
value: ''
|
||||
@ -314,7 +336,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
let filters: any = copyRuleForm.filters;
|
||||
// remove the filters which user not set.
|
||||
for (let i = filters.length - 1; i >= 0; i--) {
|
||||
if (filters[i].value === "") {
|
||||
if (filters[i].value === "" || (filters[i].value instanceof Array
|
||||
&& filters[i].value.length === 0)) {
|
||||
copyRuleForm.filters.splice(i, 1);
|
||||
}
|
||||
}
|
||||
@ -356,6 +379,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
this.inlineAlert.close();
|
||||
this.noSelectedEndpoint = true;
|
||||
this.isRuleNameValid = true;
|
||||
this.supportedFilterLabels = [];
|
||||
|
||||
|
||||
this.policyId = -1;
|
||||
this.createEditRuleOpened = true;
|
||||
@ -373,7 +398,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
this.repService.getRegistryInfo(srcRegistryId)
|
||||
.pipe(finalize(() => (this.onGoing = false)))
|
||||
.subscribe(adapter => {
|
||||
this.setFilterAndTrigger(adapter);
|
||||
this.setFilterAndTrigger(adapter, ruleInfo);
|
||||
this.updateRuleFormAndCopyUpdateForm(ruleInfo);
|
||||
}, (error: any) => {
|
||||
this.inlineAlert.showInlineError(error);
|
||||
@ -397,17 +422,63 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
setFilterAndTrigger(adapter) {
|
||||
setFilterAndTrigger(adapter, ruleInfo?) {
|
||||
this.supportedFilters = adapter.supported_resource_filters;
|
||||
this.setFilter([]);
|
||||
this.supportedFilters.forEach(element => {
|
||||
this.filters.push(this.initFilter(element.type));
|
||||
// get supportedFilterLabels labels from supportedFilters
|
||||
this.getLabelListFromAdapter(element);
|
||||
// only when edit replication rule
|
||||
if (ruleInfo && this.supportedFilterLabels.length) {
|
||||
this.getLabelListFromRuleInfo(ruleInfo);
|
||||
}
|
||||
});
|
||||
|
||||
this.supportedTriggers = adapter.supported_triggers;
|
||||
this.ruleForm.get("trigger").get("type").setValue(this.supportedTriggers[0]);
|
||||
}
|
||||
getLabelListFromAdapter(supportedFilter) {
|
||||
if (supportedFilter.type === FilterType.LABEL && supportedFilter.values) {
|
||||
this.supportedFilterLabels = [];
|
||||
supportedFilter.values.forEach( value => {
|
||||
this.supportedFilterLabels.push({
|
||||
name: value,
|
||||
color: '#fff',
|
||||
select: false,
|
||||
scope: 'g'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
getLabelListFromRuleInfo(ruleInfo) {
|
||||
let labelValueObj = ruleInfo.filters.find((currentValue) => {
|
||||
return currentValue.type === FilterType.LABEL;
|
||||
});
|
||||
if (labelValueObj) {
|
||||
for (const labelValue of labelValueObj.value) {
|
||||
let flagLabel = this.supportedFilterLabels.every((currentValue) => {
|
||||
return currentValue.name !== labelValue;
|
||||
});
|
||||
if (flagLabel) {
|
||||
this.supportedFilterLabels = [
|
||||
{
|
||||
name: labelValue,
|
||||
color: '#fff',
|
||||
select: true,
|
||||
scope: 'g'
|
||||
}, ...this.supportedFilterLabels];
|
||||
}
|
||||
//
|
||||
for (const labelObj of this.supportedFilterLabels) {
|
||||
if (labelObj.name === labelValue) {
|
||||
labelObj.select = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
close(): void {
|
||||
this.createEditRuleOpened = false;
|
||||
}
|
||||
@ -462,7 +533,11 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (!findTag) {
|
||||
filtersArray.push({ type: this.supportedFilters[i].type, value: "" });
|
||||
if (this.supportedFilters[i].type === FilterType.LABEL) {
|
||||
filtersArray.push({ type: this.supportedFilters[i].type, value: [] });
|
||||
} else {
|
||||
filtersArray.push({ type: this.supportedFilters[i].type, value: "" });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -482,4 +557,20 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
return trigger_settingsControls.controls.cron.touched || trigger_settingsControls.controls.cron.dirty;
|
||||
}
|
||||
stickLabel(value, index) {
|
||||
value.select = !value.select;
|
||||
let filters = this.ruleForm.get('filters') as FormArray;
|
||||
let fromIndex = filters.controls[index] as FormGroup;
|
||||
let labelValue = this.supportedFilterLabels.reduce( (cumulatedSelectedArrs, currentValue) => {
|
||||
if (currentValue.select) {
|
||||
if (!cumulatedSelectedArrs.length) {
|
||||
return [currentValue.name];
|
||||
}
|
||||
return [...cumulatedSelectedArrs, currentValue.name];
|
||||
}
|
||||
return cumulatedSelectedArrs;
|
||||
}, []);
|
||||
|
||||
fromIndex.setControl('value', this.fb.array(labelValue));
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ export interface ReplicationRule extends Base {
|
||||
|
||||
export class Filter {
|
||||
type: string;
|
||||
value?: string;
|
||||
value?: any;
|
||||
constructor(type: string) {
|
||||
this.type = type;
|
||||
}
|
||||
@ -434,3 +434,8 @@ export interface HttpOptionTextInterface {
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectRootInterface {
|
||||
NAME: string;
|
||||
VALUE: number;
|
||||
LABEL: string;
|
||||
}
|
||||
|
@ -61,6 +61,12 @@ export const CommonRoutes = {
|
||||
export const enum ConfirmationState {
|
||||
NA, CONFIRMED, CANCEL
|
||||
}
|
||||
export const FilterType = {
|
||||
NAME: "name",
|
||||
TAG: "tag",
|
||||
LABEL: "label",
|
||||
RESOURCE: "resource"
|
||||
};
|
||||
|
||||
export const enum ConfirmationButtons {
|
||||
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE, REPLICATE_CANCEL, STOP_CANCEL
|
||||
@ -84,3 +90,35 @@ export const LabelColor = [
|
||||
{ 'color': '#F52F52', 'textColor': 'black' }, { 'color': '#FF5501', 'textColor': 'black' },
|
||||
{ 'color': '#F57600', 'textColor': 'black' }, { 'color': '#FFDC0B', 'textColor': 'black' },
|
||||
];
|
||||
|
||||
export const CONFIG_AUTH_MODE = {
|
||||
HTTP_AUTH: "http_auth",
|
||||
LDAP_AUTH: "ldap_auth"
|
||||
};
|
||||
export const PROJECT_ROOTS = [
|
||||
{
|
||||
NAME: "admin",
|
||||
VALUE: 1,
|
||||
LABEL: "GROUP.PROJECT_ADMIN"
|
||||
},
|
||||
{
|
||||
NAME: "master",
|
||||
VALUE: 4,
|
||||
LABEL: "GROUP.PROJECT_MASTER"
|
||||
},
|
||||
{
|
||||
NAME: "developer",
|
||||
VALUE: 2,
|
||||
LABEL: "GROUP.DEVELOPER"
|
||||
},
|
||||
{
|
||||
NAME: "guest",
|
||||
VALUE: 3,
|
||||
LABEL: "GROUP.GUEST"
|
||||
}
|
||||
];
|
||||
|
||||
export enum GroupType {
|
||||
LDAP_TYPE = 1,
|
||||
HTTP_TYPE = 2
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<div class="tip-wrapper tip-position" [style.width]='maxWidth'>
|
||||
<clr-tooltip>
|
||||
<div clrTooltipTrigger>
|
||||
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(5)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(4)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(3)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(2)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(1)'></div>
|
||||
<div clrTooltipTrigger class="tip-block">
|
||||
<div class="tip-wrapper bar-block-high" [style.width]='tipWidth(5)'></div>
|
||||
<div class="tip-wrapper bar-block-medium" [style.width]='tipWidth(4)'></div>
|
||||
<div class="tip-wrapper bar-block-low" [style.width]='tipWidth(3)'></div>
|
||||
<div class="tip-wrapper bar-block-unknown" [style.width]='tipWidth(2)'></div>
|
||||
<div class="tip-wrapper bar-block-none" [style.width]='tipWidth(1)'></div>
|
||||
</div>
|
||||
<clr-tooltip-content [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen>
|
||||
<div [ngSwitch]="scanLevel" class="bar-tooltip-font-larger">
|
||||
|
@ -19,7 +19,7 @@ import { CookieService } from 'ngx-cookie';
|
||||
import { AppConfig } from './app-config';
|
||||
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
|
||||
import { maintainUrlQueryParmas } from './shared/shared.utils';
|
||||
import { HTTP_GET_OPTIONS} from '@harbor/ui';
|
||||
import { HTTP_GET_OPTIONS , CONFIG_AUTH_MODE} from '@harbor/ui';
|
||||
import { map, catchError } from "rxjs/operators";
|
||||
import { Observable, throwError as observableThrowError } from "rxjs";
|
||||
export const systemInfoEndpoint = "/api/systeminfo";
|
||||
@ -67,7 +67,10 @@ export class AppConfigService {
|
||||
}
|
||||
|
||||
public isLdapMode(): boolean {
|
||||
return this.configurations && this.configurations.auth_mode === 'ldap_auth';
|
||||
return this.configurations && this.configurations.auth_mode === CONFIG_AUTH_MODE.LDAP_AUTH;
|
||||
}
|
||||
public isHttpAuthMode(): boolean {
|
||||
return this.configurations && this.configurations.auth_mode === CONFIG_AUTH_MODE.HTTP_AUTH;
|
||||
}
|
||||
|
||||
// Return the reconstructed admiral url
|
||||
|
@ -28,7 +28,7 @@
|
||||
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
|
||||
</a>
|
||||
<a *ngIf='isLdapMode' clrVerticalNavLink routerLink="/harbor/groups" routerLinkActive="active">
|
||||
<a *ngIf='isLdapMode || isHttpAuthMode' clrVerticalNavLink routerLink="/harbor/groups" routerLinkActive="active">
|
||||
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}}
|
||||
</a>
|
||||
|
@ -54,6 +54,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
|
||||
|
||||
searchSub: Subscription;
|
||||
searchCloseSub: Subscription;
|
||||
isLdapMode: boolean;
|
||||
isHttpAuthMode: boolean;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@ -63,6 +65,11 @@ export class HarborShellComponent implements OnInit, OnDestroy {
|
||||
private appConfigService: AppConfigService) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.appConfigService.isLdapMode()) {
|
||||
this.isLdapMode = true;
|
||||
} else if (this.appConfigService.isHttpAuthMode()) {
|
||||
this.isHttpAuthMode = true;
|
||||
}
|
||||
this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(searchEvt => {
|
||||
if (searchEvt && searchEvt.trim() !== "") {
|
||||
this.isSearchResultsOpened = true;
|
||||
@ -70,7 +77,7 @@ export class HarborShellComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.searchCloseSub = this.searchTrigger.searchCloseChan$.subscribe(close => {
|
||||
this.isSearchResultsOpened = false;
|
||||
this.isSearchResultsOpened = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -97,11 +104,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
|
||||
return account != null && account.has_admin_role;
|
||||
}
|
||||
|
||||
public get isLdapMode(): boolean {
|
||||
let appConfig = this.appConfigService.getConfig();
|
||||
return appConfig.auth_mode === 'ldap_auth';
|
||||
}
|
||||
|
||||
public get isUserExisting(): boolean {
|
||||
let account = this.session.getCurrentUser();
|
||||
return account != null;
|
||||
|
@ -1,11 +1,12 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false">
|
||||
<h3 class="modal-title" *ngIf="mode === 'create'">{{'GROUP.IMPORT_LDAP_GROUP' | translate}}</h3>
|
||||
<h3 class="modal-title" *ngIf="mode === 'create' && isLdapMode">{{'GROUP.IMPORT_LDAP_GROUP' | translate}}</h3>
|
||||
<h3 class="modal-title" *ngIf="mode === 'create' && isHttpAuthMode">{{'GROUP.IMPORT_HTTP_GROUP' | translate}}</h3>
|
||||
<h3 class="modal-title" *ngIf="mode !== 'create'">{{'GROUP.EDIT' | translate}}</h3>
|
||||
|
||||
<div class="modal-body">
|
||||
<form class="form" #groupForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="isLdapMode">
|
||||
<label for="ldap_group_dn" class="required">{{ 'GROUP.GROUP_DN' | translate}}</label>
|
||||
<label for="ldap_group_dn"
|
||||
aria-haspopup="true"
|
||||
@ -22,7 +23,7 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="isLdapMode">
|
||||
<label for="type">{{'GROUP.TYPE' | translate}}</label>
|
||||
<label id="type">LDAP</label>
|
||||
</div>
|
||||
|
@ -1,13 +1,15 @@
|
||||
|
||||
import {finalize} from 'rxjs/operators';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core";
|
||||
import { NgForm } from "@angular/forms";
|
||||
import { GroupType } from "@harbor/ui";
|
||||
|
||||
import { GroupService } from "../group.service";
|
||||
import { MessageHandlerService } from "./../../shared/message-handler/message-handler.service";
|
||||
import { SessionService } from "./../../shared/session.service";
|
||||
import { UserGroup } from "./../group";
|
||||
import { AppConfigService } from "../../app-config.service";
|
||||
|
||||
@Component({
|
||||
selector: "hbr-add-group-modal",
|
||||
@ -19,7 +21,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
mode = "create";
|
||||
dnTooltip = 'TOOLTIP.ITEM_REQUIRED';
|
||||
|
||||
group: UserGroup = new UserGroup();
|
||||
group: UserGroup;
|
||||
|
||||
formChangeSubscription: Subscription;
|
||||
|
||||
@ -30,25 +32,36 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Output() dataChange = new EventEmitter();
|
||||
|
||||
isLdapMode: boolean;
|
||||
isHttpAuthMode: boolean;
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private msgHandler: MessageHandlerService,
|
||||
private appConfigService: AppConfigService,
|
||||
private groupService: GroupService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
) { }
|
||||
|
||||
ngOnInit() { }
|
||||
ngOnInit() {
|
||||
if (this.appConfigService.isLdapMode()) {
|
||||
this.isLdapMode = true;
|
||||
}
|
||||
if (this.appConfigService.isHttpAuthMode()) {
|
||||
this.isHttpAuthMode = true;
|
||||
}
|
||||
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : GroupType.HTTP_TYPE);
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy() { }
|
||||
|
||||
public get isDNInvalid(): boolean {
|
||||
let dnControl = this.groupForm.controls['ldap_group_dn'];
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
}
|
||||
public get isNameInvalid(): boolean {
|
||||
let dnControl = this.groupForm.controls['group_name'];
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
}
|
||||
|
||||
public get isFormValid(): boolean {
|
||||
@ -83,7 +96,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
let groupCopy = Object.assign({}, this.group);
|
||||
this.groupService
|
||||
.createGroup(groupCopy).pipe(
|
||||
finalize(() => this.close()))
|
||||
finalize(() => this.close()))
|
||||
.subscribe(
|
||||
res => {
|
||||
this.msgHandler.showSuccess("GROUP.ADD_GROUP_SUCCESS");
|
||||
@ -97,7 +110,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
let groupCopy = Object.assign({}, this.group);
|
||||
this.groupService
|
||||
.editGroup(groupCopy).pipe(
|
||||
finalize(() => this.close()))
|
||||
finalize(() => this.close()))
|
||||
.subscribe(
|
||||
res => {
|
||||
this.msgHandler.showSuccess("GROUP.EDIT_GROUP_SUCCESS");
|
||||
@ -108,7 +121,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
resetGroup() {
|
||||
this.group = new UserGroup();
|
||||
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : GroupType.HTTP_TYPE);
|
||||
this.groupForm.reset();
|
||||
}
|
||||
}
|
||||
|
@ -15,18 +15,18 @@
|
||||
<clr-icon shape="plus" size="15"></clr-icon> {{'GROUP.ADD' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="editGroup()" [disabled]="!canEditGroup">
|
||||
<clr-icon shape="pencil" size="15"></clr-icon> {{'GROUP.EDIT' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="openDeleteConfirmationDialog()" [disabled]="!canEditGroup">
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="openDeleteConfirmationDialog()" [disabled]="!canDeleteGroup">
|
||||
<clr-icon shape="times" size="15"></clr-icon> {{'GROUP.DELETE' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
|
||||
<clr-dg-column>{{'GROUP.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'GROUP.TYPE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'GROUP.DN' | translate}}</clr-dg-column>
|
||||
<clr-dg-column *ngIf="isLdapMode">{{'GROUP.DN' | translate}}</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let group of groups" [clrDgItem]="group">
|
||||
<clr-dg-cell>{{group.group_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{groupToSring(group.group_type) | translate}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{group.ldap_group_dn}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="isLdapMode">{{group.ldap_group_dn}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="15">
|
||||
|
@ -4,7 +4,7 @@ import { flatMap, catchError } from "rxjs/operators";
|
||||
import { SessionService } from "./../shared/session.service";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { Component, OnInit, ViewChild, OnDestroy } from "@angular/core";
|
||||
import { operateChanges, OperateInfo, OperationService, OperationState, errorHandler as errorHandFn } from "@harbor/ui";
|
||||
import { operateChanges, OperateInfo, OperationService, OperationState, errorHandler as errorHandFn, GroupType } from "@harbor/ui";
|
||||
|
||||
import {
|
||||
ConfirmationTargets,
|
||||
@ -19,6 +19,8 @@ import { UserGroup } from "./group";
|
||||
import { GroupService } from "./group.service";
|
||||
import { MessageHandlerService } from "../shared/message-handler/message-handler.service";
|
||||
import { throwError as observableThrowError } from "rxjs";
|
||||
import { AppConfigService } from '../app-config.service';
|
||||
|
||||
@Component({
|
||||
selector: "app-group",
|
||||
templateUrl: "./group.component.html",
|
||||
@ -35,6 +37,7 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
delSub: Subscription;
|
||||
batchOps = 'idle';
|
||||
batchInfos = new Map();
|
||||
isLdapMode: boolean;
|
||||
|
||||
@ViewChild(AddGroupModalComponent) newGroupModal: AddGroupModalComponent;
|
||||
|
||||
@ -46,10 +49,14 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
private msgHandler: MessageHandlerService,
|
||||
private session: SessionService,
|
||||
private translateService: TranslateService,
|
||||
private appConfigService: AppConfigService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.loadData();
|
||||
if (this.appConfigService.isLdapMode()) {
|
||||
this.isLdapMode = true;
|
||||
}
|
||||
this.delSub = this.operateDialogService.confirmationConfirm$.subscribe(
|
||||
message => {
|
||||
if (
|
||||
@ -150,7 +157,13 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
groupToSring(type: number) {
|
||||
if (type === 1) { return 'GROUP.LDAP_TYPE'; } else { return 'UNKNOWN'; }
|
||||
if (type === GroupType.LDAP_TYPE) {
|
||||
return 'GROUP.LDAP_TYPE';
|
||||
} else if (type === GroupType.HTTP_TYPE) {
|
||||
return 'GROUP.HTTP_TYPE';
|
||||
} else {
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
doFilter(groupName: string): void {
|
||||
@ -162,6 +175,12 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get canEditGroup(): boolean {
|
||||
return (
|
||||
this.selectedGroups.length === 1 &&
|
||||
this.session.currentUser.has_admin_role && this.isLdapMode
|
||||
);
|
||||
}
|
||||
get canDeleteGroup(): boolean {
|
||||
return (
|
||||
this.selectedGroups.length === 1 &&
|
||||
this.session.currentUser.has_admin_role
|
||||
|
@ -4,9 +4,9 @@ export class UserGroup {
|
||||
group_type: number;
|
||||
ldap_group_dn?: string;
|
||||
|
||||
constructor() {
|
||||
constructor(groupType) {
|
||||
{
|
||||
this.group_type = 1;
|
||||
this.group_type = groupType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export class AddGroupComponent implements OnInit {
|
||||
currentTerm = '';
|
||||
|
||||
selectedRole = 1;
|
||||
group = new UserGroup();
|
||||
group = new UserGroup(1);
|
||||
selectedGroups: UserGroup[] = [];
|
||||
groups: UserGroup[] = [];
|
||||
totalCount = 0;
|
||||
@ -89,7 +89,7 @@ export class AddGroupComponent implements OnInit {
|
||||
|
||||
resetModaldata() {
|
||||
this.createGroupMode = false;
|
||||
this.group = new UserGroup();
|
||||
this.group = new UserGroup(1);
|
||||
this.selectedRole = 1;
|
||||
this.selectedGroups = [];
|
||||
this.groups = [];
|
||||
|
@ -0,0 +1,36 @@
|
||||
<clr-modal [(clrModalOpen)]="addHttpAuthOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{'GROUP.NEW_MEMBER' | translate}}</h3>
|
||||
<inline-alert class="modal-title padding-0"></inline-alert>
|
||||
<div class="modal-body">
|
||||
<label>{{ 'GROUP.NEW_USER_INFO' | translate}}</label>
|
||||
|
||||
<form #memberForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="member_name" class="col-md-4 form-group-label-override required">{{'GROUP.GROUP' | translate}} {{'GROUP.NAME' | translate}}</label>
|
||||
<label for="member_name" aria-haspopup="true" role="tooltip"
|
||||
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
|
||||
<input type="text" id="member_name" [(ngModel)]="member_group.group_name"
|
||||
name="member_name"
|
||||
size="20"
|
||||
minlength="3"
|
||||
#memberName="ngModel"
|
||||
required autocomplete="off">
|
||||
</label>
|
||||
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'GROUP.ROLE' | translate}}</label>
|
||||
<div class="radio" *ngFor="let projectRoot of projectRoots">
|
||||
<input type="radio" name="member_role" id="{{'check_root_project_' + projectRoot.NAME}}" [value]="projectRoot.VALUE" [(ngModel)]="role_id">
|
||||
<label for="{{'check_root_project_' + projectRoot.NAME}}">{{ projectRoot.LABEL | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -0,0 +1,8 @@
|
||||
.form-group-label-override {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.padding-0 {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddHttpAuthGroupComponent } from './add-http-auth-group.component';
|
||||
|
||||
describe('AddHttpAuthGroupComponent', () => {
|
||||
let component: AddHttpAuthGroupComponent;
|
||||
let fixture: ComponentFixture<AddHttpAuthGroupComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddHttpAuthGroupComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddHttpAuthGroupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,120 @@
|
||||
import { finalize } from 'rxjs/operators';
|
||||
// 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.
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
EventEmitter,
|
||||
Output,
|
||||
ViewChild,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { InlineAlertComponent } from '../../../shared/inline-alert/inline-alert.component';
|
||||
import { UserService } from '../../../user/user.service';
|
||||
|
||||
|
||||
import { errorHandler as errorHandFn, PROJECT_ROOTS, ProjectRootInterface } from "@harbor/ui";
|
||||
|
||||
import { MemberService } from '../member.service';
|
||||
import { UserGroup } from "./../../../group/group";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'add-http-auth-group',
|
||||
templateUrl: './add-http-auth-group.component.html',
|
||||
styleUrls: ['./add-http-auth-group.component.scss'],
|
||||
providers: [UserService]
|
||||
})
|
||||
|
||||
export class AddHttpAuthGroupComponent implements OnInit {
|
||||
projectRoots: ProjectRootInterface[];
|
||||
member_group: UserGroup = { group_name: '', group_type: 2 };
|
||||
role_id: number;
|
||||
addHttpAuthOpened: boolean;
|
||||
|
||||
memberForm: NgForm;
|
||||
|
||||
staticBackdrop: boolean = true;
|
||||
closable: boolean = false;
|
||||
|
||||
@ViewChild('memberForm')
|
||||
currentForm: NgForm;
|
||||
|
||||
@ViewChild(InlineAlertComponent)
|
||||
inlineAlert: InlineAlertComponent;
|
||||
|
||||
@Input() projectId: number;
|
||||
@Output() added = new EventEmitter<boolean>();
|
||||
|
||||
checkOnGoing: boolean = false;
|
||||
|
||||
constructor(private memberService: MemberService,
|
||||
private translateService: TranslateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectRoots = PROJECT_ROOTS;
|
||||
}
|
||||
|
||||
createGroupAsMember() {
|
||||
this.checkOnGoing = true;
|
||||
this.memberService.addGroupMember(this.projectId, this.member_group, this.role_id)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.checkOnGoing = false;
|
||||
}
|
||||
))
|
||||
.subscribe(
|
||||
res => {
|
||||
this.role_id = null;
|
||||
this.addHttpAuthOpened = false;
|
||||
this.added.emit(true);
|
||||
},
|
||||
err => {
|
||||
let errorMessageKey: string = errorHandFn(err);
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(errorMessage => this.inlineAlert.showInlineError(errorMessage));
|
||||
this.added.emit(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
onSubmit(): void {
|
||||
this.createGroupAsMember();
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.role_id = null;
|
||||
this.addHttpAuthOpened = false;
|
||||
}
|
||||
|
||||
|
||||
openAddMemberModal(): void {
|
||||
this.currentForm.reset();
|
||||
this.addHttpAuthOpened = true;
|
||||
this.role_id = 1;
|
||||
}
|
||||
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.currentForm &&
|
||||
this.currentForm.valid &&
|
||||
!this.checkOnGoing;
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
<button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasCreateMemberPermission">
|
||||
<span><clr-icon shape="plus" size="16"></clr-icon> {{'MEMBER.USER' | translate }}</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" (click)="openAddGroupModal()" [disabled]="!hasCreateMemberPermission || !isLdapMode">
|
||||
<button class="btn btn-sm btn-secondary" (click)="openAddGroupModal()" [disabled]="!hasCreateMemberPermission || !(isLdapMode || isHttpAuthMode)">
|
||||
<span><clr-icon shape="plus" size="16"></clr-icon> {{'MEMBER.LDAP_GROUP' | translate}}</span>
|
||||
</button>
|
||||
<clr-dropdown id='member-action' [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-link" clrDropdownTrigger>
|
||||
@ -53,4 +53,5 @@
|
||||
</div>
|
||||
<add-member [projectId]="projectId" [memberList]="members" (added)="addedMember($event)"></add-member>
|
||||
<add-group [projectId]="projectId" [memberList]="members" (added)="addedGroup($event)"></add-group>
|
||||
<add-http-auth-group [projectId]="projectId" (added)="addedGroup($event)"></add-http-auth-group>
|
||||
</div>
|
@ -17,8 +17,10 @@ import { Component, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, Chang
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subscription, forkJoin, Observable } from "rxjs";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { operateChanges, OperateInfo, OperationService, OperationState, UserPermissionService, USERSTATICPERMISSION, ErrorHandler
|
||||
, errorHandler as errorHandFn } from "@harbor/ui";
|
||||
import {
|
||||
operateChanges, OperateInfo, OperationService, OperationState, UserPermissionService, USERSTATICPERMISSION, ErrorHandler
|
||||
, errorHandler as errorHandFn
|
||||
} from "@harbor/ui";
|
||||
|
||||
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
|
||||
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from "../../shared/shared.const";
|
||||
@ -30,6 +32,7 @@ import { Project } from "../../project/project";
|
||||
import { Member } from "./member";
|
||||
import { SessionUser } from "../../shared/session-user";
|
||||
import { AddGroupComponent } from './add-group/add-group.component';
|
||||
import { AddHttpAuthGroupComponent } from './add-http-auth-group/add-http-auth-group.component';
|
||||
import { MemberService } from "./member.service";
|
||||
import { AddMemberComponent } from "./add-member/add-member.component";
|
||||
import { AppConfigService } from "../../app-config.service";
|
||||
@ -56,16 +59,18 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
isDelete = false;
|
||||
isChangeRole = false;
|
||||
loading = false;
|
||||
isLdapMode: boolean = false;
|
||||
|
||||
isChangingRole = false;
|
||||
batchChangeRoleInfos = {};
|
||||
|
||||
isLdapMode: boolean;
|
||||
isHttpAuthMode: boolean;
|
||||
@ViewChild(AddMemberComponent)
|
||||
addMemberComponent: AddMemberComponent;
|
||||
|
||||
@ViewChild(AddGroupComponent)
|
||||
addGroupComponent: AddGroupComponent;
|
||||
@ViewChild(AddHttpAuthGroupComponent)
|
||||
addHttpAuthGroupComponent: AddHttpAuthGroupComponent;
|
||||
hasCreateMemberPermission: boolean;
|
||||
hasUpdateMemberPermission: boolean;
|
||||
hasDeleteMemberPermission: boolean;
|
||||
@ -108,13 +113,15 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
// Get current user from registered resolver.
|
||||
this.currentUser = this.session.getCurrentUser();
|
||||
this.retrieve(this.projectId, "");
|
||||
// get member permission rule
|
||||
this.getMemberPermissionRule(this.projectId);
|
||||
if (this.appConfigService.isLdapMode()) {
|
||||
this.isLdapMode = true;
|
||||
}
|
||||
// get member permission rule
|
||||
this.getMemberPermissionRule(this.projectId);
|
||||
if (this.appConfigService.isHttpAuthMode()) {
|
||||
this.isHttpAuthMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
doSearch(searchMember: string) {
|
||||
this.searchMember = searchMember;
|
||||
this.retrieve(this.projectId, this.searchMember);
|
||||
@ -172,7 +179,11 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Add group
|
||||
openAddGroupModal() {
|
||||
this.addGroupComponent.open();
|
||||
if (this.isLdapMode) {
|
||||
this.addGroupComponent.open();
|
||||
} else {
|
||||
this.addHttpAuthGroupComponent.openAddMemberModal();
|
||||
}
|
||||
}
|
||||
addedGroup(result: boolean) {
|
||||
this.searchMember = "";
|
||||
@ -188,10 +199,10 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
return this.memberService
|
||||
.changeMemberRole(projectId, member.id, roleId)
|
||||
.pipe(map(() => this.batchChangeRoleInfos[member.id] = 'done')
|
||||
, catchError(error => {
|
||||
this.messageHandlerService.handleError(error + ": " + member.entity_name);
|
||||
return observableThrowError(error);
|
||||
}));
|
||||
, catchError(error => {
|
||||
this.messageHandlerService.handleError(error + ": " + member.entity_name);
|
||||
return observableThrowError(error);
|
||||
}));
|
||||
};
|
||||
|
||||
// Preparation for members role change
|
||||
|
@ -38,6 +38,7 @@ import { ProjectLabelComponent } from "../project/project-label/project-label.co
|
||||
import { HelmChartModule } from './helm-chart/helm-chart.module';
|
||||
import { RobotAccountComponent } from './robot-account/robot-account.component';
|
||||
import { AddRobotComponent } from './robot-account/add-robot/add-robot.component';
|
||||
import { AddHttpAuthGroupComponent } from './member/add-http-auth-group/add-http-auth-group.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -59,7 +60,8 @@ import { AddRobotComponent } from './robot-account/add-robot/add-robot.component
|
||||
ProjectLabelComponent,
|
||||
AddGroupComponent,
|
||||
RobotAccountComponent,
|
||||
AddRobotComponent
|
||||
AddRobotComponent,
|
||||
AddHttpAuthGroupComponent
|
||||
],
|
||||
exports: [ProjectComponent, ListProjectComponent],
|
||||
providers: [ProjectRoutingResolver, MemberService, RobotService]
|
||||
|
@ -1,130 +1,129 @@
|
||||
<clr-modal [(clrModalOpen)]="addRobotOpened"
|
||||
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{'ROBOT_ACCOUNT.CREAT_ROBOT_ACCOUNT' | translate}}</h3>
|
||||
<inline-alert #copyAlert class="modal-title"></inline-alert>
|
||||
<div class="modal-body">
|
||||
<form #robotForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3
|
||||
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{'ROBOT_ACCOUNT.CREAT_ROBOT_ACCOUNT' | translate}}</h3>
|
||||
<inline-alert #copyAlert class="modal-title"></inline-alert>
|
||||
<div class="modal-body">
|
||||
<form #robotForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group padding-left-120">
|
||||
<label class="col-md-3
|
||||
form-group-label-override required" for="robot_name">
|
||||
{{'ROBOT_ACCOUNT.NAME' | translate}}
|
||||
</label>
|
||||
<label aria-haspopup="true" role="tooltip" class="tooltip
|
||||
{{'ROBOT_ACCOUNT.NAME' | translate}}
|
||||
</label>
|
||||
<label aria-haspopup="true" role="tooltip" class="tooltip
|
||||
tooltip-validation
|
||||
tooltip-md tooltip-bottom-left" for="robot_name"
|
||||
[class.invalid]="!isRobotNameValid">
|
||||
<input type="text"
|
||||
[(ngModel)]="robot.name"
|
||||
size="30" class="input-width"
|
||||
name="robot_name"
|
||||
id="robot_name"
|
||||
#robotName="ngModel"
|
||||
required
|
||||
pattern='[^" ~#$%]+'
|
||||
maxLengthExt="255"
|
||||
autocomplete="off"
|
||||
(keyup)='handleValidation()'>
|
||||
<span class="tooltip-content">
|
||||
[class.invalid]="!isRobotNameValid">
|
||||
<input type="text"
|
||||
[(ngModel)]="robot.name"
|
||||
size="30" class="input-width"
|
||||
name="robot_name"
|
||||
id="robot_name"
|
||||
#robotName="ngModel"
|
||||
required
|
||||
pattern='[^" ~#$%]+'
|
||||
maxLengthExt="255"
|
||||
autocomplete="off"
|
||||
(keyup)='handleValidation()'>
|
||||
<span class="tooltip-content">
|
||||
{{ nameTooltipText | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' |
|
||||
translate}}</label>
|
||||
<input type="text" size="255" class="input-width"
|
||||
[(ngModel)]="robot.description"
|
||||
name="robot_desc" id="robot_desc">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3">
|
||||
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
|
||||
</label>
|
||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [checked]="true"
|
||||
[(ngModel)]="robot.access.isPullImage" name="isPullImage"
|
||||
id="permission-pull" class="clr-checkbox">
|
||||
<label for="permission-pull" class="clr-control-label">
|
||||
{{'ROBOT_ACCOUNT.PULL_PERMISSION' | translate}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</label>
|
||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [checked]="true"
|
||||
[(ngModel)]="robot.access.isPushOrPullImage" name="isPushOrPullImage"
|
||||
id="permission-push" class="clr-checkbox">
|
||||
<label for="permission-push" class="clr-control-label">
|
||||
{{'ROBOT_ACCOUNT.PULL_PUSH_PERMISSION' | translate}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</label>
|
||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [checked]="true"
|
||||
[(ngModel)]="robot.access.isPushChart" name="isPushChart"
|
||||
id="permission-push-chart" class="clr-checkbox">
|
||||
<label for="permission-push-chart" class="clr-control-label">
|
||||
{{'ROBOT_ACCOUNT.PUSH_CHART_PERMISSION' | translate}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</label>
|
||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [checked]="true"
|
||||
[(ngModel)]="robot.access.isPullChart" name="isPullChart"
|
||||
id="permission-pull-chart" class="clr-checkbox">
|
||||
<label for="permission-pull-chart" class="clr-control-label">
|
||||
{{'ROBOT_ACCOUNT.PULL_CHART_PERMISSION' | translate}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL'
|
||||
| translate}}</button>
|
||||
<button type="button" [disabled]="shouldDisable" class="btn btn-primary"
|
||||
(click)="onSubmit()">{{'BUTTON.SAVE'
|
||||
| translate}}</button>
|
||||
</div>
|
||||
</label>
|
||||
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
|
||||
</div>
|
||||
<div class="form-group padding-left-120">
|
||||
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' |
|
||||
translate}}</label>
|
||||
<input type="text" size="255" class="input-width"
|
||||
[(ngModel)]="robot.description"
|
||||
name="robot_desc" id="robot_desc">
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-3 permission">
|
||||
<label class="col-md-3">
|
||||
{{'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>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL'
|
||||
| translate}}</button>
|
||||
<button type="button" [disabled]="shouldDisable" class="btn btn-primary"
|
||||
(click)="onSubmit()">{{'BUTTON.SAVE'
|
||||
| translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
<clr-modal [(clrModalOpen)]="copyToken" class="copy-token"
|
||||
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<div class="modal-title">
|
||||
<h3 class="modal-title">
|
||||
<clr-icon class="alert-icon success-icon" shape="check-circle" size="50"></clr-icon>
|
||||
{{ createSuccess | translate}}</h3>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="alert-icon" shape="info-circle"></clr-icon>
|
||||
</div>
|
||||
<span class="alert-text">{{'ROBOT_ACCOUNT.ALERT_TEXT' | translate}}</span>
|
||||
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<div class="modal-title">
|
||||
<h3 class="modal-title">
|
||||
<clr-icon class="alert-icon success-icon" shape="check-circle" size="50"></clr-icon>
|
||||
{{ createSuccess | translate}}</h3>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="alert-icon" shape="info-circle"></clr-icon>
|
||||
</div>
|
||||
<span class="alert-text">{{'ROBOT_ACCOUNT.ALERT_TEXT' | translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<section class="form-block show-info">
|
||||
<div class="form-group robot-name">
|
||||
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.NAME'
|
||||
| translate}}</label>
|
||||
<span>{{robotAccount}}</span>
|
||||
</div>
|
||||
<div class="form-group robot-token">
|
||||
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.TOKEN' |
|
||||
translate}}</label>
|
||||
<hbr-copy-input (onCopySuccess)="onCpSuccess($event)"
|
||||
(onCopyError)="onCpError($event)" inputSize="50" headerTitle=""
|
||||
defaultValue="{{robotToken}}" class="copy-input"></hbr-copy-input>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<section class="form-block show-info">
|
||||
<div class="form-group robot-name">
|
||||
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.NAME'
|
||||
| translate}}</label>
|
||||
<span>{{robotAccount}}</span>
|
||||
</div>
|
||||
<div class="form-group robot-token">
|
||||
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.TOKEN' |
|
||||
translate}}</label>
|
||||
<hbr-copy-input (onCopySuccess)="onCpSuccess($event)"
|
||||
(onCopyError)="onCpError($event)" inputSize="50" headerTitle=""
|
||||
defaultValue="{{robotToken}}" class="copy-input"></hbr-copy-input>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</clr-modal>
|
@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.input-width {
|
||||
width: 200px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.copy-token {
|
||||
@ -35,3 +35,12 @@
|
||||
.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.permission{
|
||||
padding-top: 5px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.padding-left-120{
|
||||
padding-left: 120px;
|
||||
}
|
@ -38,17 +38,18 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
||||
robotNameChecker: Subject<string> = new Subject<string>();
|
||||
nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
||||
robotForm: NgForm;
|
||||
imagePermission: string = "push-and-pull";
|
||||
@Input() projectId: number;
|
||||
@Input() projectName: string;
|
||||
@Output() create = new EventEmitter<boolean>();
|
||||
@ViewChild("robotForm") currentForm: NgForm;
|
||||
@ViewChild("copyAlert") copyAlert: InlineAlertComponent;
|
||||
constructor(
|
||||
private robotService: RobotService,
|
||||
private translate: TranslateService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private messageHandlerService: MessageHandlerService
|
||||
private robotService: RobotService,
|
||||
private translate: TranslateService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private messageHandlerService: MessageHandlerService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -59,31 +60,31 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
||||
if (this.isRobotNameValid) {
|
||||
this.checkOnGoing = true;
|
||||
this.robotService
|
||||
.listRobotAccount(this.projectId)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.checkOnGoing = false;
|
||||
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 2000);
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
response => {
|
||||
if (response && response.length) {
|
||||
if (
|
||||
response.find(target => {
|
||||
return target.name === "robot$" + cont.value;
|
||||
})
|
||||
) {
|
||||
this.isRobotNameValid = false;
|
||||
this.nameTooltipText = "ROBOT_ACCOUNT.ACCOUNT_EXISTING";
|
||||
.listRobotAccount(this.projectId)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.checkOnGoing = false;
|
||||
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 2000);
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
response => {
|
||||
if (response && response.length) {
|
||||
if (
|
||||
response.find(target => {
|
||||
return target.name === "robot$" + cont.value;
|
||||
})
|
||||
) {
|
||||
this.isRobotNameValid = false;
|
||||
this.nameTooltipText = "ROBOT_ACCOUNT.ACCOUNT_EXISTING";
|
||||
}
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
);
|
||||
} else {
|
||||
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
||||
}
|
||||
@ -116,49 +117,57 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
||||
if (this.isSubmitOnGoing) {
|
||||
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 {
|
||||
this.robot.access.isPullImage = false;
|
||||
this.robot.access.isPushOrPullImage = true;
|
||||
}
|
||||
this.isSubmitOnGoing = true;
|
||||
this.robotService
|
||||
.addRobotAccount(
|
||||
this.projectId,
|
||||
this.robot,
|
||||
this.projectName
|
||||
)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.isSubmitOnGoing = false;
|
||||
this.robotToken = response.token;
|
||||
this.robotAccount = response.name;
|
||||
this.copyToken = true;
|
||||
this.create.emit(true);
|
||||
this.translate
|
||||
.get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: this.robotAccount })
|
||||
.subscribe((res: string) => {
|
||||
this.createSuccess = res;
|
||||
});
|
||||
this.addRobotOpened = false;
|
||||
},
|
||||
error => {
|
||||
this.isSubmitOnGoing = false;
|
||||
this.copyAlert.showInlineError(error);
|
||||
}
|
||||
);
|
||||
.addRobotAccount(
|
||||
this.projectId,
|
||||
this.robot,
|
||||
this.projectName
|
||||
)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.isSubmitOnGoing = false;
|
||||
this.robotToken = response.token;
|
||||
this.robotAccount = response.name;
|
||||
this.copyToken = true;
|
||||
this.create.emit(true);
|
||||
this.translate
|
||||
.get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: this.robotAccount })
|
||||
.subscribe((res: string) => {
|
||||
this.createSuccess = res;
|
||||
});
|
||||
this.addRobotOpened = false;
|
||||
},
|
||||
error => {
|
||||
this.isSubmitOnGoing = false;
|
||||
this.copyAlert.showInlineError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
return (
|
||||
this.currentForm &&
|
||||
this.currentForm.valid &&
|
||||
!this.isSubmitOnGoing &&
|
||||
this.isRobotNameValid &&
|
||||
!this.checkOnGoing
|
||||
this.currentForm &&
|
||||
this.currentForm.valid &&
|
||||
!this.isSubmitOnGoing &&
|
||||
this.isRobotNameValid &&
|
||||
!this.checkOnGoing
|
||||
);
|
||||
}
|
||||
get shouldDisable(): boolean {
|
||||
if (this.robot && this.robot.access) {
|
||||
return (
|
||||
!this.isValid() ||
|
||||
(!this.robot.access.isPushOrPullImage && !this.robot.access.isPullImage
|
||||
&& !this.robot.access.isPullChart && !this.robot.access.isPushChart)
|
||||
!this.isValid() ||
|
||||
(!this.robot.access.isPushOrPullImage && !this.robot.access.isPullImage
|
||||
&& !this.robot.access.isPullChart && !this.robot.access.isPushChart)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -180,9 +189,9 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
||||
onCpSuccess($event: any): void {
|
||||
this.copyToken = false;
|
||||
this.translate
|
||||
.get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robotAccount })
|
||||
.subscribe((res: string) => {
|
||||
this.messageHandlerService.showSuccess(res);
|
||||
});
|
||||
.get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robotAccount })
|
||||
.subscribe((res: string) => {
|
||||
this.messageHandlerService.showSuccess(res);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export class Robot {
|
||||
constructor () {
|
||||
this.access = <any>{};
|
||||
// this.access[0].action = true;
|
||||
this.access.isPullImage = true;
|
||||
this.access.isPullImage = false;
|
||||
this.access.isPushOrPullImage = true;
|
||||
this.access.isPushChart = false;
|
||||
this.access.isPullChart = false;
|
||||
|
@ -58,6 +58,7 @@
|
||||
"TOOLTIP": {
|
||||
"NAME_FILTER": "Filter the name of the resource. Leave empty or use '**' to match all. 'library/**' only matches resources under 'library'. For more patterns, please refer to the user guide.",
|
||||
"TAG_FILTER": "Filter the tag/version part of the resources. Leave empty or use '**' to match all. '1.0*' only matches the tags that starts with '1.0'. For more patterns, please refer to the user guide.",
|
||||
"LABEL_FILTER": "Filter the resources according to labels.",
|
||||
"RESOURCE_FILTER": "Filter the type of resources.",
|
||||
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.",
|
||||
"PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
|
||||
@ -310,10 +311,10 @@
|
||||
"ENABLE_ACCOUNT": "Enable Account",
|
||||
"DELETE": "Delete",
|
||||
"CREAT_ROBOT_ACCOUNT": "Creat Robot Account",
|
||||
"PULL_PERMISSION": "Image pull",
|
||||
"PULL_PUSH_PERMISSION": "Image pull / push",
|
||||
"PUSH_CHART_PERMISSION": "Helm chart push",
|
||||
"PULL_CHART_PERMISSION": "Helm chart pull",
|
||||
"PERMISSIONS_IMAGE": "Image",
|
||||
"PERMISSIONS_HELMCHART": "Helm Chart",
|
||||
"PUSH": "Push",
|
||||
"PULL": "Pull",
|
||||
"FILTER_PLACEHOLDER": "Filter Robot Accounts",
|
||||
"ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.",
|
||||
"ACCOUNT_EXISTING": "Robot Account is already exists.",
|
||||
@ -327,6 +328,7 @@
|
||||
"GROUP": "Group",
|
||||
"GROUPS": "Groups",
|
||||
"IMPORT_LDAP_GROUP": "Import LDAP Group",
|
||||
"IMPORT_HTTP_GROUP": "New HTTP Group",
|
||||
"ADD": "New Group",
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete",
|
||||
@ -339,8 +341,17 @@
|
||||
"ADD_GROUP_SUCCESS": "Add group success",
|
||||
"EDIT_GROUP_SUCCESS": "Edit group success",
|
||||
"LDAP_TYPE": "LDAP",
|
||||
"HTTP_TYPE": "HTTP",
|
||||
"OF": "of",
|
||||
"ITEMS": "items"
|
||||
"ITEMS": "items",
|
||||
"NEW_MEMBER": "New Group Member",
|
||||
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
|
||||
"ROLE": "Role",
|
||||
"SYS_ADMIN": "System Admin",
|
||||
"PROJECT_ADMIN": "Project Admin",
|
||||
"PROJECT_MASTER": "Master",
|
||||
"DEVELOPER": "Developer",
|
||||
"GUEST": "Guest"
|
||||
},
|
||||
"AUDIT_LOG": {
|
||||
"USERNAME": "Username",
|
||||
|
@ -58,6 +58,7 @@
|
||||
"TOOLTIP": {
|
||||
"NAME_FILTER": "Filter the name of the resource. Leave empty or use '**' to match all. 'library/**' only matches resources under 'library'. For more patterns, please refer to the user guide.",
|
||||
"TAG_FILTER": "Filter the tag/version part of the resources. Leave empty or use '**' to match all. '1.0*' only matches the tags that starts with '1.0'. For more patterns, please refer to the user guide.",
|
||||
"LABEL_FILTER": "Filter the resources according to labels.",
|
||||
"RESOURCE_FILTER": "Filter the type of resources.",
|
||||
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.",
|
||||
"PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
|
||||
@ -311,10 +312,10 @@
|
||||
"ENABLE_ACCOUNT": "Enable Account",
|
||||
"DELETE": "Delete",
|
||||
"CREAT_ROBOT_ACCOUNT": "Creat Robot Account",
|
||||
"PULL_PERMISSION": "Image pull",
|
||||
"PULL_PUSH_PERMISSION": "Image pull / push",
|
||||
"PUSH_CHART_PERMISSION": "Helm chart push",
|
||||
"PULL_CHART_PERMISSION": "Helm chart pull",
|
||||
"PERMISSIONS_IMAGE": "Image",
|
||||
"PERMISSIONS_HELMCHART": "Helm Chart",
|
||||
"PUSH": "Push",
|
||||
"PULL": "Pull",
|
||||
"FILTER_PLACEHOLDER": "Filter Robot Accounts",
|
||||
"ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.",
|
||||
"ACCOUNT_EXISTING": "Robot Account is already exists.",
|
||||
@ -328,6 +329,7 @@
|
||||
"GROUP": "Group",
|
||||
"GROUPS": "Groups",
|
||||
"IMPORT_LDAP_GROUP": "Import LDAP Group",
|
||||
"IMPORT_HTTP_GROUP": "New HTTP Group",
|
||||
"ADD": "Add",
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete",
|
||||
@ -339,8 +341,17 @@
|
||||
"ADD_GROUP_SUCCESS": "Add group success",
|
||||
"EDIT_GROUP_SUCCESS": "Edit group success",
|
||||
"LDAP_TYPE": "LDAP",
|
||||
"HTTP_TYPE": "HTTP",
|
||||
"OF": "of",
|
||||
"ITEMS": "items"
|
||||
"ITEMS": "items",
|
||||
"NEW_MEMBER": "New Group Member",
|
||||
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
|
||||
"ROLE": "Role",
|
||||
"SYS_ADMIN": "System Admin",
|
||||
"PROJECT_ADMIN": "Project Admin",
|
||||
"PROJECT_MASTER": "Master",
|
||||
"DEVELOPER": "Developer",
|
||||
"GUEST": "Guest"
|
||||
},
|
||||
"AUDIT_LOG": {
|
||||
"USERNAME": "Nombre de usuario",
|
||||
|
@ -55,6 +55,7 @@
|
||||
"TOOLTIP": {
|
||||
"NAME_FILTER": "Filter the name of the resource. Leave empty or use '**' to match all. 'library/**' only matches resources under 'library'. For more patterns, please refer to the user guide.",
|
||||
"TAG_FILTER": "Filter the tag/version part of the resources. Leave empty or use '**' to match all. '1.0*' only matches the tags that starts with '1.0'. For more patterns, please refer to the user guide.",
|
||||
"LABEL_FILTER": "Filter the resources according to labels.",
|
||||
"RESOURCE_FILTER": "Filter the type of resources.",
|
||||
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.",
|
||||
"PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
|
||||
@ -302,10 +303,11 @@
|
||||
"ENABLE_ACCOUNT": "permettre à compte ",
|
||||
"DELETE": "Supprimer",
|
||||
"CREAT_ROBOT_ACCOUNT": "créat robot compte ",
|
||||
"PULL_PERMISSION": "Image pull",
|
||||
"PULL_PUSH_PERMISSION": "Image pull / push",
|
||||
"PUSH_CHART_PERMISSION": "Helm chart push",
|
||||
"PULL_CHART_PERMISSION": "Helm chart pull",
|
||||
"PERMISSIONS_IMAGE": "Image",
|
||||
"PERMISSIONS_HELMCHART": "Helm Chart",
|
||||
"PUSH": "Push",
|
||||
"PULL": "Pull",
|
||||
|
||||
"FILTER_PLACEHOLDER": "Filter Robot Accounts",
|
||||
"ROBOT_NAME": "ne peut pas contenir de caractères spéciaux(~#$%) et la longueur maximale devrait être de 255 caractères.",
|
||||
"ACCOUNT_EXISTING": "le robot est existe déjà.",
|
||||
@ -319,6 +321,7 @@
|
||||
"Group": "Group",
|
||||
"GROUPS": "Groups",
|
||||
"IMPORT_LDAP_GROUP": "Import LDAP Group",
|
||||
"IMPORT_HTTP_GROUP": "New HTTP Group",
|
||||
"ADD": "Add",
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete",
|
||||
@ -331,8 +334,17 @@
|
||||
"ADD_GROUP_SUCCESS": "Add group success",
|
||||
"EDIT_GROUP_SUCCESS": "Edit group success",
|
||||
"LDAP_TYPE": "LDAP",
|
||||
"HTTP_TYPE": "HTTP",
|
||||
"OF": "of",
|
||||
"ITEMS": "items"
|
||||
"ITEMS": "items",
|
||||
"NEW_MEMBER": "New Group Member",
|
||||
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
|
||||
"ROLE": "Role",
|
||||
"SYS_ADMIN": "System Admin",
|
||||
"PROJECT_ADMIN": "Project Admin",
|
||||
"PROJECT_MASTER": "Master",
|
||||
"DEVELOPER": "Developer",
|
||||
"GUEST": "Guest"
|
||||
},
|
||||
"AUDIT_LOG": {
|
||||
"USERNAME": "Nom d'utilisateur",
|
||||
|
@ -58,6 +58,7 @@
|
||||
"TOOLTIP": {
|
||||
"NAME_FILTER": "Filter the name of the resource. Leave empty or use '**' to match all. 'library/**' only matches resources under 'library'. For more patterns, please refer to the user guide.",
|
||||
"TAG_FILTER": "Filter the tag/version part of the resources. Leave empty or use '**' to match all. '1.0*' only matches the tags that starts with '1.0'. For more patterns, please refer to the user guide.",
|
||||
"LABEL_FILTER": "Filter the resources according to labels.",
|
||||
"RESOURCE_FILTER": "Filter the type of resources.",
|
||||
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.",
|
||||
"PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
|
||||
@ -308,10 +309,10 @@
|
||||
"ENABLE_ACCOUNT": "Ativar conta",
|
||||
"DELETE": "Remover",
|
||||
"CREAT_ROBOT_ACCOUNT": "CRIA robô conta",
|
||||
"PULL_PERMISSION": "Image pull",
|
||||
"PULL_PUSH_PERMISSION": "Image pull / push",
|
||||
"PUSH_CHART_PERMISSION": "Helm chart push",
|
||||
"PULL_CHART_PERMISSION": "Helm chart pull",
|
||||
"PERMISSIONS_IMAGE": "Image",
|
||||
"PERMISSIONS_HELMCHART": "Helm Chart",
|
||||
"PUSH": "Push",
|
||||
"PULL": "Pull",
|
||||
"FILTER_PLACEHOLDER": "Filtro robot accounts",
|
||||
"ROBOT_NAME": "Não Pode conter caracteres especiais(~#$%) e comprimento máximo deveria ser 255 caracteres.",
|
||||
"ACCOUNT_EXISTING": "Robô conta já existe.",
|
||||
@ -325,6 +326,7 @@
|
||||
"GROUP": "Grupo",
|
||||
"GROUPS": "Grupos",
|
||||
"IMPORT_LDAP_GROUP": "Importar grupo do LDAP",
|
||||
"IMPORT_HTTP_GROUP": "New HTTP Group",
|
||||
"ADD": "Novo Grupo",
|
||||
"EDIT": "Editar",
|
||||
"DELETE": "Remover",
|
||||
@ -337,8 +339,17 @@
|
||||
"ADD_GROUP_SUCCESS": "Grupo adicionado com sucesso",
|
||||
"EDIT_GROUP_SUCCESS": "Grupo editado com sucesso",
|
||||
"LDAP_TYPE": "LDAP",
|
||||
"HTTP_TYPE": "HTTP",
|
||||
"OF": "de",
|
||||
"ITEMS": "itens"
|
||||
"ITEMS": "itens",
|
||||
"NEW_MEMBER": "New Group Member",
|
||||
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
|
||||
"ROLE": "Role",
|
||||
"SYS_ADMIN": "System Admin",
|
||||
"PROJECT_ADMIN": "Project Admin",
|
||||
"PROJECT_MASTER": "Master",
|
||||
"DEVELOPER": "Developer",
|
||||
"GUEST": "Guest"
|
||||
},
|
||||
"AUDIT_LOG": {
|
||||
"USERNAME": "Nome do usuário",
|
||||
|
@ -58,6 +58,7 @@
|
||||
"TOOLTIP": {
|
||||
"NAME_FILTER": "过滤资源的名字。不填或者“”匹配所有资源;“library/”只匹配“library”下的资源。更多的匹配模式请参考用户手册。",
|
||||
"TAG_FILTER": "过滤资源的tag/version。不填或者“”匹配所有;“1.0*”只匹配以“1.0”开头的tag/version。",
|
||||
"LABEL_FILTER": "根据标签筛选资源。",
|
||||
"RESOURCE_FILTER": "过滤资源的类型。",
|
||||
"PUSH_BASED": "把资源由本地Harbor推送到远端仓库。",
|
||||
"PULL_BASED": "把资源由远端仓库拉取到本地Harbor。",
|
||||
@ -86,8 +87,8 @@
|
||||
"NONEMPTY": "不能为空",
|
||||
"ENDPOINT_FORMAT": "Endpoint必须以http://或https://开头。",
|
||||
"OIDC_ENDPOIT_FORMAT": "Endpoint必须以https://开头。",
|
||||
"OIDC_NAME": "OIDC提供商的名称.",
|
||||
"OIDC_ENDPOINT": "OIDC服务器的地址.",
|
||||
"OIDC_NAME": "OIDC提供商的名称。",
|
||||
"OIDC_ENDPOINT": "OIDC服务器的地址。",
|
||||
"OIDC_SCOPE": "在身份验证期间发送到OIDC服务器的scope。它必须包含“openid”和“offline_access”。如果您使用Google,请从此字段中删除“脱机访问”。",
|
||||
"OIDC_VERIFYCERT": "如果您的OIDC服务器是通过自签名证书托管的,请取消选中此框。"
|
||||
},
|
||||
@ -298,7 +299,7 @@
|
||||
"NEW_ROBOT_ACCOUNT": "添加机器人账户",
|
||||
"ENABLED_STATE": "启用状态",
|
||||
"EXPIRATION": "过期时间",
|
||||
"NUMBER_REQUIRED":"此项为必填项且为不为0的整数.",
|
||||
"NUMBER_REQUIRED":"此项为必填项且为不为0的整数。",
|
||||
"TOKEN_EXPIRATION":"机器人账户令牌过期时间(天)",
|
||||
"DESCRIPTION": "描述",
|
||||
"ACTION": "操作",
|
||||
@ -309,15 +310,15 @@
|
||||
"ENABLE_ACCOUNT": "启用账户",
|
||||
"DELETE": "删除",
|
||||
"CREAT_ROBOT_ACCOUNT": "创建机器人账户",
|
||||
"PULL_PERMISSION": "Pull 镜像",
|
||||
"PULL_PUSH_PERMISSION": "Push和Pull 镜像",
|
||||
"PUSH_CHART_PERMISSION": "推送Chart",
|
||||
"PULL_CHART_PERMISSION": "拉取Chart",
|
||||
"PERMISSIONS_IMAGE": "镜像",
|
||||
"PERMISSIONS_HELMCHART": "Helm Chart",
|
||||
"PUSH": "推送",
|
||||
"PULL": "拉取",
|
||||
"FILTER_PLACEHOLDER": "过滤机器人账户",
|
||||
"ROBOT_NAME": "不能包含特殊字符(~#$%)且长度不能超过255.",
|
||||
"ACCOUNT_EXISTING": "机器人账户已经存在.",
|
||||
"ROBOT_NAME": "不能包含特殊字符(~#$%)且长度不能超过255。",
|
||||
"ACCOUNT_EXISTING": "机器人账户已经存在。",
|
||||
"ALERT_TEXT": "这是唯一一次复制您的个人访问令牌的机会",
|
||||
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功.",
|
||||
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功。",
|
||||
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
|
||||
"DELETION_TITLE": "删除账户确认",
|
||||
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?"
|
||||
@ -326,6 +327,7 @@
|
||||
"GROUP": "组",
|
||||
"GROUPS": "组",
|
||||
"IMPORT_LDAP_GROUP": "导入LDAP组",
|
||||
"IMPORT_HTTP_GROUP": "新建HTTP组",
|
||||
"ADD": "新增",
|
||||
"EDIT": "编辑",
|
||||
"DELETE": "删除",
|
||||
@ -338,8 +340,17 @@
|
||||
"ADD_GROUP_SUCCESS": "添加组成功",
|
||||
"EDIT_GROUP_SUCCESS": "修改组成功",
|
||||
"LDAP_TYPE": "LDAP",
|
||||
"HTTP_TYPE": "HTTP",
|
||||
"OF": "共计",
|
||||
"ITEMS": "条记录"
|
||||
"ITEMS": "条记录",
|
||||
"NEW_MEMBER": "新建组成员",
|
||||
"NEW_USER_INFO": "添加一个组作为具有指定角色的此项目的成员",
|
||||
"ROLE": "权限",
|
||||
"SYS_ADMIN": "系统管理员",
|
||||
"PROJECT_ADMIN": "项目管理员",
|
||||
"PROJECT_MASTER": "维护人员",
|
||||
"DEVELOPER": "开发者",
|
||||
"GUEST": "访客"
|
||||
},
|
||||
"AUDIT_LOG": {
|
||||
"USERNAME": "用户名",
|
||||
@ -727,7 +738,7 @@
|
||||
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间(分钟),默认为30分钟。",
|
||||
"ROBOT_TOKEN_EXPIRATION": "机器人账户的令牌的过期时间(天),默认为30天,显示的结果为分钟转化的天数并向下取整。",
|
||||
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
|
||||
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书.",
|
||||
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书。",
|
||||
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。",
|
||||
"VERIFY_CERT": "检查来自LDAP服务端的证书",
|
||||
"READONLY_TOOLTIP": "选中,表示正在维护状态,不可删除仓库及标签,也不可以推送镜像。",
|
||||
@ -859,8 +870,8 @@
|
||||
},
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "扫描完成时间:",
|
||||
"TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}.",
|
||||
"TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
|
||||
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞包"
|
||||
},
|
||||
"SEVERITY": {
|
||||
|
@ -86,4 +86,8 @@ body {
|
||||
|
||||
.color-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.datagrid-table,.datagrid-header{
|
||||
position: inherit !important;
|
||||
}
|
||||
|
@ -17,10 +17,18 @@ package adapter
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/goharbor/harbor/src/replication/filter"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
|
||||
// const definition
|
||||
const (
|
||||
UserAgentReplication = "harbor-replication-service"
|
||||
)
|
||||
|
||||
var registry = map[model.RegistryType]Factory{}
|
||||
|
||||
// Factory creates a specific Adapter according to the params
|
||||
@ -37,6 +45,81 @@ type Adapter interface {
|
||||
HealthCheck() (model.HealthStatus, error)
|
||||
}
|
||||
|
||||
// ImageRegistry defines the capabilities that an image registry should have
|
||||
type ImageRegistry interface {
|
||||
FetchImages(filters []*model.Filter) ([]*model.Resource, error)
|
||||
ManifestExist(repository, reference string) (exist bool, digest string, err error)
|
||||
PullManifest(repository, reference string, accepttedMediaTypes []string) (manifest distribution.Manifest, digest string, err error)
|
||||
PushManifest(repository, reference, mediaType string, payload []byte) error
|
||||
// the "reference" can be "tag" or "digest", the function needs to handle both
|
||||
DeleteManifest(repository, reference string) error
|
||||
BlobExist(repository, digest string) (exist bool, err error)
|
||||
PullBlob(repository, digest string) (size int64, blob io.ReadCloser, err error)
|
||||
PushBlob(repository, digest string, size int64, blob io.Reader) error
|
||||
}
|
||||
|
||||
// ChartRegistry defines the capabilities that a chart registry should have
|
||||
type ChartRegistry interface {
|
||||
FetchCharts(filters []*model.Filter) ([]*model.Resource, error)
|
||||
ChartExist(name, version string) (bool, error)
|
||||
DownloadChart(name, version string) (io.ReadCloser, error)
|
||||
UploadChart(name, version string, chart io.Reader) error
|
||||
DeleteChart(name, version string) error
|
||||
}
|
||||
|
||||
// Repository defines an repository object, it can be image repository, chart repository and etc.
|
||||
type Repository struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// GetName returns the name
|
||||
func (r *Repository) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
// GetFilterableType returns the filterable type
|
||||
func (r *Repository) GetFilterableType() filter.FilterableType {
|
||||
return filter.FilterableTypeRepository
|
||||
}
|
||||
|
||||
// GetResourceType returns the resource type
|
||||
func (r *Repository) GetResourceType() string {
|
||||
return r.ResourceType
|
||||
}
|
||||
|
||||
// GetLabels returns the labels
|
||||
func (r *Repository) GetLabels() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// VTag defines an vTag object, it can be image tag, chart version and etc.
|
||||
type VTag struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
Name string `json:"name"`
|
||||
Labels []string `json:"labels"`
|
||||
}
|
||||
|
||||
// GetFilterableType returns the filterable type
|
||||
func (v *VTag) GetFilterableType() filter.FilterableType {
|
||||
return filter.FilterableTypeVTag
|
||||
}
|
||||
|
||||
// GetResourceType returns the resource type
|
||||
func (v *VTag) GetResourceType() string {
|
||||
return v.ResourceType
|
||||
}
|
||||
|
||||
// GetName returns the name
|
||||
func (v *VTag) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// GetLabels returns the labels
|
||||
func (v *VTag) GetLabels() []string {
|
||||
return v.Labels
|
||||
}
|
||||
|
||||
// RegisterFactory registers one adapter factory to the registry
|
||||
func RegisterFactory(t model.RegistryType, factory Factory) error {
|
||||
if len(t) == 0 {
|
||||
|
176
src/replication/adapter/awsecr/adapter.go
Normal file
176
src/replication/adapter/awsecr/adapter.go
Normal file
@ -0,0 +1,176 @@
|
||||
// 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 awsecr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
awsecrapi "github.com/aws/aws-sdk-go/service/ecr"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||
"github.com/goharbor/harbor/src/replication/adapter/native"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := adp.RegisterFactory(model.RegistryTypeAwsEcr, func(registry *model.Registry) (adp.Adapter, error) {
|
||||
return newAdapter(registry)
|
||||
}); err != nil {
|
||||
log.Errorf("failed to register factory for %s: %v", model.RegistryTypeAwsEcr, err)
|
||||
return
|
||||
}
|
||||
log.Infof("the factory for adapter %s registered", model.RegistryTypeAwsEcr)
|
||||
}
|
||||
|
||||
func newAdapter(registry *model.Registry) (*adapter, error) {
|
||||
region, err := parseRegion(registry.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizer := NewAuth(region, registry.Credential.AccessKey, registry.Credential.AccessSecret, registry.Insecure)
|
||||
dockerRegistry, err := native.NewAdapterWithCustomizedAuthorizer(registry, authorizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adapter{
|
||||
registry: registry,
|
||||
Adapter: dockerRegistry,
|
||||
region: region,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseRegion(url string) (string, error) {
|
||||
pattern := "https://(?:api|\\d+\\.dkr)\\.ecr\\.([\\w\\-]+)\\.amazonaws\\.com"
|
||||
rs := regexp.MustCompile(pattern).FindStringSubmatch(url)
|
||||
if rs == nil {
|
||||
return "", errors.New("Bad aws url")
|
||||
}
|
||||
return rs[1], nil
|
||||
}
|
||||
|
||||
type adapter struct {
|
||||
*native.Adapter
|
||||
registry *model.Registry
|
||||
region string
|
||||
forceEndpoint *string
|
||||
}
|
||||
|
||||
func (*adapter) Info() (info *model.RegistryInfo, err error) {
|
||||
return &model.RegistryInfo{
|
||||
Type: model.RegistryTypeAwsEcr,
|
||||
SupportedResourceTypes: []model.ResourceType{
|
||||
model.ResourceTypeImage,
|
||||
},
|
||||
SupportedResourceFilters: []*model.FilterStyle{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Style: model.FilterStyleTypeText,
|
||||
},
|
||||
{
|
||||
Type: model.FilterTypeTag,
|
||||
Style: model.FilterStyleTypeText,
|
||||
},
|
||||
},
|
||||
SupportedTriggers: []model.TriggerType{
|
||||
model.TriggerTypeManual,
|
||||
model.TriggerTypeScheduled,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HealthCheck checks health status of a registry
|
||||
func (a *adapter) HealthCheck() (model.HealthStatus, error) {
|
||||
if a.registry.Credential == nil ||
|
||||
len(a.registry.Credential.AccessKey) == 0 || len(a.registry.Credential.AccessSecret) == 0 {
|
||||
log.Errorf("no credential to ping registry %s", a.registry.URL)
|
||||
return model.Unhealthy, nil
|
||||
}
|
||||
if err := a.PingGet(); err != nil {
|
||||
log.Errorf("failed to ping registry %s: %v", a.registry.URL, err)
|
||||
return model.Unhealthy, nil
|
||||
}
|
||||
return model.Healthy, nil
|
||||
}
|
||||
|
||||
// PrepareForPush nothing need to do.
|
||||
func (a *adapter) PrepareForPush(resources []*model.Resource) error {
|
||||
for _, resource := range resources {
|
||||
if resource == nil {
|
||||
return errors.New("the resource cannot be nil")
|
||||
}
|
||||
if resource.Metadata == nil {
|
||||
return errors.New("the metadata of resource cannot be nil")
|
||||
}
|
||||
if resource.Metadata.Repository == nil {
|
||||
return errors.New("the namespace of resource cannot be nil")
|
||||
}
|
||||
if len(resource.Metadata.Repository.Name) == 0 {
|
||||
return errors.New("the name of the namespace cannot be nil")
|
||||
}
|
||||
|
||||
err := a.createRepository(resource.Metadata.Repository.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) createRepository(repository string) error {
|
||||
if a.registry.Credential == nil ||
|
||||
len(a.registry.Credential.AccessKey) == 0 || len(a.registry.Credential.AccessSecret) == 0 {
|
||||
return errors.New("no credential ")
|
||||
}
|
||||
cred := credentials.NewStaticCredentials(
|
||||
a.registry.Credential.AccessKey,
|
||||
a.registry.Credential.AccessSecret,
|
||||
"")
|
||||
if a.region == "" {
|
||||
return errors.New("no region parsed")
|
||||
}
|
||||
config := &aws.Config{
|
||||
Credentials: cred,
|
||||
Region: &a.region,
|
||||
HTTPClient: &http.Client{
|
||||
Transport: registry.GetHTTPTransport(a.registry.Insecure),
|
||||
},
|
||||
}
|
||||
if a.forceEndpoint != nil {
|
||||
config.Endpoint = a.forceEndpoint
|
||||
}
|
||||
sess := session.Must(session.NewSession(config))
|
||||
|
||||
svc := awsecrapi.New(sess)
|
||||
|
||||
_, err := svc.CreateRepository(&awsecrapi.CreateRepositoryInput{
|
||||
RepositoryName: &repository,
|
||||
})
|
||||
if err != nil {
|
||||
if e, ok := err.(awserr.Error); ok {
|
||||
if e.Code() == awsecrapi.ErrCodeRepositoryAlreadyExistsException {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user