Merge pull request #1 from goharbor/master

update
This commit is contained in:
Chenyu Zhang 2019-07-12 09:50:47 +08:00 committed by GitHub
commit 1e2660f060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
288 changed files with 38718 additions and 1887 deletions

View File

@ -292,7 +292,7 @@ compile_notary_migrate_patch:
compile: check_environment versions_prepare compile_core compile_jobservice compile_registryctl compile_notary_migrate_patch compile: check_environment versions_prepare compile_core compile_jobservice compile_registryctl compile_notary_migrate_patch
update_prepare_version: 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 ; @$(SEDCMD) -i -e 's/goharbor\/prepare:.*[[:space:]]\+/goharbor\/prepare:$(VERSIONTAG) /' $(MAKEPATH)/prepare ;
prepare: update_prepare_version prepare: update_prepare_version
@ -416,7 +416,7 @@ start:
@echo "Start complete. You can visit harbor now." @echo "Start complete. You can visit harbor now."
down: 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 \ @while [ -z "$$CONTINUE" ]; do \
read -r -p "Type anything but Y or y to exit. [Y/N]: " CONTINUE; \ read -r -p "Type anything but Y or y to exit. [Y/N]: " CONTINUE; \
done ; \ done ; \

View File

@ -44,25 +44,25 @@ You can compile the code by one of the three approaches:
* Get official Golang image from docker hub: * Get official Golang image from docker hub:
```sh ```sh
$ docker pull golang:1.11.2 $ docker pull golang:1.12.5
``` ```
* Build, install and bring up Harbor without Notary: * Build, install and bring up Harbor without Notary:
```sh ```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: * Build, install and bring up Harbor with Notary:
```sh ```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: * Build, install and bring up Harbor with Clair:
```sh ```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 #### II. Compile code with your own Golang environment, then build Harbor

View File

@ -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 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_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_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_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 * 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. 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 Base DN -- ldap_group_basedn in the Harbor user settings
- LDAP Group Filter -- ldap_group_filter in harbor.cfg - LDAP Group Filter -- ldap_group_filter in the Harbor user settings
- LDAP Group GID -- ldap_group_gid in harbor.cfg - LDAP Group GID -- ldap_group_gid in the Harbor user settings
- LDAP Group Scope -- ldap_group_scope in harbor.cfg - 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. - 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) ![Screenshot of LDAP group config](img/group/ldap_group_config.png)

View File

@ -2,7 +2,7 @@ swagger: '2.0'
info: info:
title: Harbor API title: Harbor API
description: These APIs provide services for manipulating Harbor project. description: These APIs provide services for manipulating Harbor project.
version: 1.8.0 version: 1.9.0
host: localhost host: localhost
schemes: schemes:
- http - http
@ -3478,6 +3478,44 @@ paths:
description: The robot account is not found. description: The robot account is not found.
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
'/system/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: responses:
OK: OK:
description: 'Success' description: 'Success'
@ -3601,6 +3639,9 @@ definitions:
metadata: metadata:
description: The metadata of the project. description: The metadata of the project.
$ref: '#/definitions/ProjectMetadata' $ref: '#/definitions/ProjectMetadata'
cve_whitelist:
description: The CVE whitelist of this project.
$ref: '#/definitions/CVEWhitelist'
ProjectMetadata: ProjectMetadata:
type: object type: object
properties: properties:
@ -5070,3 +5111,27 @@ definitions:
metadata: metadata:
type: object 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"

View File

@ -36,10 +36,10 @@ version | set harbor version
#### EXAMPLE: #### EXAMPLE:
#### Build and run harbor from source code. #### 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 ### 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 ### Start harbor with notary
make -e NOTARYFLAG=true start make -e NOTARYFLAG=true start

View 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)
);

View File

@ -8,5 +8,6 @@ RUN mkdir /harbor/ \
COPY ./make/photon/jobservice/start.sh ./make/photon/jobservice/harbor_jobservice /harbor/ COPY ./make/photon/jobservice/start.sh ./make/photon/jobservice/harbor_jobservice /harbor/
RUN chmod u+x /harbor/harbor_jobservice /harbor/start.sh RUN chmod u+x /harbor/harbor_jobservice /harbor/start.sh
RUN mkdir -p /var/log/jobs
WORKDIR /harbor/ WORKDIR /harbor/
ENTRYPOINT ["/harbor/start.sh"] ENTRYPOINT ["/harbor/start.sh"]

View 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
}

View 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"))
}

View File

@ -15,10 +15,8 @@
package group package group
import ( import (
"strings"
"time" "time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
@ -139,20 +137,3 @@ func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttri
return nil 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, ",")
}

View File

@ -47,6 +47,8 @@ func TestMain(m *testing.M) {
initSqls := []string{ 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 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 ('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')", "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'", "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)", "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{ clearSqls := []string{
"delete from project where name='member_test_01'", "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 harbor_user where username='member_test_01' or username='pm_sample'",
"delete from user_group", "delete from user_group",
"delete from project_member", "delete from project_member",
@ -175,7 +179,7 @@ func TestUpdateUserGroup(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
fmt.Printf("id=%v", createdUserGroupID) fmt.Printf("id=%v\n", createdUserGroupID)
if err := UpdateUserGroupName(tt.args.id, tt.args.groupName); (err != nil) != tt.wantErr { if err := UpdateUserGroupName(tt.args.id, tt.args.groupName); (err != nil) != tt.wantErr {
t.Errorf("UpdateUserGroup() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("UpdateUserGroup() error = %v, wantErr %v", err, tt.wantErr)
userGroup, err := GetUserGroup(tt.args.id) 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) { func TestGetGroupProjects(t *testing.T) {
userID, err := dao.Register(models.User{ userID, err := dao.Register(models.User{
Username: "grouptestu09", Username: "grouptestu09",
@ -322,7 +292,6 @@ func TestGetGroupProjects(t *testing.T) {
}) })
defer project.DeleteProjectMemberByID(pmid) defer project.DeleteProjectMemberByID(pmid)
type args struct { type args struct {
groupDNCondition string
query *models.ProjectQueryParam query *models.ProjectQueryParam
} }
member := &models.MemberQuery{ member := &models.MemberQuery{
@ -335,19 +304,17 @@ func TestGetGroupProjects(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{"Query with group DN", {"Query with group DN",
args{"'cn=harbor_users,ou=groups,dc=example,dc=com'", args{&models.ProjectQueryParam{
&models.ProjectQueryParam{
Member: member, Member: member,
}}, }},
1, false}, 1, false},
{"Query without group DN", {"Query without group DN",
args{"", args{&models.ProjectQueryParam{}},
&models.ProjectQueryParam{}},
1, false}, 1, false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if (err != nil) != tt.wantErr {
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
return return
@ -392,7 +359,6 @@ func TestGetTotalGroupProjects(t *testing.T) {
}) })
defer project.DeleteProjectMemberByID(pmid) defer project.DeleteProjectMemberByID(pmid)
type args struct { type args struct {
groupDNCondition string
query *models.ProjectQueryParam query *models.ProjectQueryParam
} }
tests := []struct { tests := []struct {
@ -401,18 +367,16 @@ func TestGetTotalGroupProjects(t *testing.T) {
wantSize int wantSize int
wantErr bool wantErr bool
}{ }{
{"Query with group DN", {"Query with group ID",
args{"'cn=harbor_users,ou=groups,dc=example,dc=com'", args{&models.ProjectQueryParam{}},
&models.ProjectQueryParam{}},
1, false}, 1, false},
{"Query without group DN", {"Query without group ID",
args{"", args{&models.ProjectQueryParam{}},
&models.ProjectQueryParam{}},
1, false}, 1, false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if (err != nil) != tt.wantErr {
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
return 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)
}
})
}
}

View File

@ -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 // 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. // 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, params := projectQueryConditions(query)
sql = `select distinct p.project_id, p.name, p.owner_id, sql = `select distinct p.project_id, p.name, p.owner_id,
p.creation_time, p.update_time ` + sql p.creation_time, p.update_time ` + sql
if len(groupDNCondition) > 0 { groupIDCondition := JoinNumberConditions(groupIDs)
if len(groupIDs) > 0 {
sql = fmt.Sprintf( sql = fmt.Sprintf(
`%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time `%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time
from project p from project p
left join project_member pm on p.project_id = pm.project_id 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 left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
where ug.ldap_group_dn in ( %s ) order by name`, where ug.id in ( %s ) order by name`,
sql, groupDNCondition) sql, groupIDCondition)
} }
sqlStr, queryParams := CreatePagination(query, sql, params) sqlStr, queryParams := CreatePagination(query, sql, params)
log.Debugf("query sql:%v", sql) 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 // 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. // 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 var sql string
sqlCondition, params := projectQueryConditions(query) sqlCondition, params := projectQueryConditions(query)
if len(groupDNCondition) == 0 { groupIDCondition := JoinNumberConditions(groupIDs)
if len(groupIDs) == 0 {
sql = `select count(1) ` + sqlCondition sql = `select count(1) ` + sqlCondition
} else { } else {
sql = fmt.Sprintf( 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 ( select p.project_id %s union select p.project_id
from project p from project p
left join project_member pm on p.project_id = pm.project_id 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 left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
where ug.ldap_group_dn in ( %s )) t`, where ug.id in ( %s )) t`,
sqlCondition, groupDNCondition) sqlCondition, groupIDCondition)
} }
log.Debugf("query sql:%v", sql) log.Debugf("query sql:%v", sql)
var count int var count int
@ -291,24 +293,24 @@ func DeleteProject(id int64) error {
return err return err
} }
// GetRolesByLDAPGroup - Get Project roles of the // GetRolesByGroupID - Get Project roles of the
// specified group DN is a member of current project // specified group is a member of current project
func GetRolesByLDAPGroup(projectID int64, groupDNCondition string) ([]int, error) { func GetRolesByGroupID(projectID int64, groupIDs []int) ([]int, error) {
var roles []int var roles []int
if len(groupDNCondition) == 0 { if len(groupIDs) == 0 {
return roles, nil return roles, nil
} }
groupIDCondition := JoinNumberConditions(groupIDs)
o := GetOrmer() 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. // 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( sql := fmt.Sprintf(
`select min(pm.role) from project_member pm `select min(pm.role) from project_member pm
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id 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 = ? `, where ug.id in ( %s ) and pm.project_id = ?`,
groupDNCondition) groupIDCondition)
log.Debugf("sql:%v", sql) log.Debugf("sql:%v", sql)
if _, err := o.Raw(sql, projectID).QueryRows(&roles); err != nil { 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 return nil, err
} }
// If there is no row selected, the min returns an empty row, to avoid return 0 as role // If there is no row selected, the min returns an empty row, to avoid return 0 as role

View File

@ -148,16 +148,3 @@ func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, e
_, err := o.Raw(sql, queryParam).QueryRows(&members) _, err := o.Raw(sql, queryParam).QueryRows(&members)
return members, err 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
}

View File

@ -305,30 +305,3 @@ func PrepareGroupTest() {
} }
dao.PrepareTestData(clearSqls, initSqls) 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)
}
})
}
}

View File

@ -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() { func prepareGroupTest() {
initSqls := []string{ 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')`, `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) 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) { func TestProjetExistsByName(t *testing.T) {
name := "project_exist_by_name_test" name := "project_exist_by_name_test"
exist := ProjectExistsByName(name) exist := ProjectExistsByName(name)

View File

@ -15,12 +15,11 @@
package dao package dao
import ( import (
"encoding/json"
"fmt"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"encoding/json"
"fmt"
"time" "time"
) )

11
src/common/dao/utils.go Normal file
View 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), "[]")
}

View 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)
}
})
}
}

View File

@ -36,5 +36,6 @@ func init() {
new(AdminJob), new(AdminJob),
new(JobLog), new(JobLog),
new(Robot), new(Robot),
new(OIDCUser)) new(OIDCUser),
new(CVEWhitelist))
} }

View 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
}

View 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()))
}
}

View File

@ -25,6 +25,7 @@ const (
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
ProMetaSeverity = "severity" ProMetaSeverity = "severity"
ProMetaAutoScan = "auto_scan" ProMetaAutoScan = "auto_scan"
ProMetaReuseSysCVEWhitelist = "reuse_sys_cve_whitelist"
SeverityNone = "negligible" SeverityNone = "negligible"
SeverityLow = "low" SeverityLow = "low"
SeverityMedium = "medium" SeverityMedium = "medium"

View File

@ -36,6 +36,7 @@ type Project struct {
RepoCount int64 `orm:"-" json:"repo_count"` RepoCount int64 `orm:"-" json:"repo_count"`
ChartCount uint64 `orm:"-" json:"chart_count"` ChartCount uint64 `orm:"-" json:"chart_count"`
Metadata map[string]string `orm:"-" json:"metadata"` Metadata map[string]string `orm:"-" json:"metadata"`
CVEWhitelist CVEWhitelist `orm:"-" json:"cve_whitelist"`
} }
// GetMetadata ... // GetMetadata ...
@ -83,6 +84,15 @@ func (p *Project) VulPrevented() bool {
return isTrue(prevent) return isTrue(prevent)
} }
// ReuseSysCVEWhitelist ...
func (p *Project) ReuseSysCVEWhitelist() bool {
r, ok := p.GetMetadata(ProMetaReuseSysCVEWhitelist)
if !ok {
return true
}
return isTrue(r)
}
// Severity ... // Severity ...
func (p *Project) Severity() string { func (p *Project) Severity() string {
severity, exist := p.GetMetadata(ProMetaSeverity) severity, exist := p.GetMetadata(ProMetaSeverity)
@ -130,7 +140,7 @@ type ProjectQueryParam struct {
type MemberQuery struct { type MemberQuery struct {
Name string // the username of member Name string // the username of member
Role int // the role of the member has to the project Role int // the role of the member has to the project
GroupList []*UserGroup // the group list of current user GroupIDs []int // the group ID of current user belongs to
} }
// Pagination ... // Pagination ...
@ -157,6 +167,7 @@ type ProjectRequest struct {
Name string `json:"project_name"` Name string `json:"project_name"`
Public *int `json:"public"` // deprecated, reserved for project creation in replication Public *int `json:"public"` // deprecated, reserved for project creation in replication
Metadata map[string]string `json:"metadata"` Metadata map[string]string `json:"metadata"`
CVEWhitelist CVEWhitelist `json:"cve_whitelist"`
} }
// ProjectQueryResult ... // ProjectQueryResult ...

View File

@ -34,31 +34,6 @@ type ScanJob struct {
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` 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 // TableName is required by by beego orm to map ScanJob to table img_scan_job
func (s *ScanJob) TableName() string { func (s *ScanJob) TableName() string {
return ScanJobTable return ScanJobTable
@ -101,17 +76,6 @@ type ImageScanReq struct {
Tag string `json:"tag"` 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 // ScanAllPolicy is represent the json request and object for scan all policy, the parm is het
type ScanAllPolicy struct { type ScanAllPolicy struct {
Type string `json:"type"` Type string `json:"type"`

26
src/common/models/sev.go Normal file
View 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"
}
}

View File

@ -40,7 +40,7 @@ type User struct {
Salt string `orm:"column(salt)" json:"-"` Salt string `orm:"column(salt)" json:"-"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` 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"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
GroupList []*UserGroup `orm:"-" json:"-"` GroupIDs []int `orm:"-" json:"-"`
OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta,omitempty"` OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta,omitempty"`
} }

View File

@ -54,6 +54,7 @@ var (
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete}, {Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate}, {Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete}, {Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionList}, {Resource: rbac.ResourceMember, Action: rbac.ActionList},

View File

@ -27,6 +27,7 @@ var (
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete}, {Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate}, {Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete}, {Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionList}, {Resource: rbac.ResourceMember, Action: rbac.ActionList},
@ -105,6 +106,7 @@ var (
"master": { "master": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, {Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList}, {Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate}, {Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
@ -172,6 +174,7 @@ var (
"developer": { "developer": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, {Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList}, {Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceLog, Action: rbac.ActionList}, {Resource: rbac.ResourceLog, Action: rbac.ActionList},
@ -221,6 +224,7 @@ var (
"guest": { "guest": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, {Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList}, {Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceLog, Action: rbac.ActionList}, {Resource: rbac.ResourceLog, Action: rbac.ActionList},

View File

@ -17,7 +17,6 @@ package local
import ( import (
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/group"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/common/rbac/project"
@ -140,12 +139,11 @@ func (s *SecurityContext) GetRolesByGroup(projectIDOrName interface{}) []int {
user := s.user user := s.user
project, err := s.pm.Get(projectIDOrName) project, err := s.pm.Get(projectIDOrName)
// No user, group or project info // 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 return roles
} }
// Get role by LDAP group // Get role by Group ID
groupDNConditions := group.GetGroupDNQueryCondition(user.GroupList) roles, err = dao.GetRolesByGroupID(project.ProjectID, user.GroupIDs)
roles, err = dao.GetRolesByLDAPGroup(project.ProjectID, groupDNConditions)
if err != nil { if err != nil {
return nil return nil
} }
@ -158,7 +156,7 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
&models.ProjectQueryParam{ &models.ProjectQueryParam{
Member: &models.MemberQuery{ Member: &models.MemberQuery{
Name: s.GetUsername(), Name: s.GetUsername(),
GroupList: s.user.GroupList, GroupIDs: s.user.GroupIDs,
}, },
}) })
if err != nil { if err != nil {

View File

@ -20,6 +20,7 @@ import (
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/group"
"github.com/goharbor/harbor/src/common/dao/project" "github.com/goharbor/harbor/src/common/dao/project"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
@ -253,9 +254,16 @@ func TestHasPushPullPermWithGroup(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Error occurred when GetUser: %v", err) 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) resource := rbac.NewProjectNamespace(project.Name).Resource(rbac.ResourceRepository)
@ -332,9 +340,15 @@ func TestSecurityContext_GetRolesByGroup(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Error occurred when GetUser: %v", err) t.Errorf("Error occurred when GetUser: %v", err)
} }
developer.GroupList = []*models.UserGroup{ userGroups, err := group.QueryUserGroup(models.UserGroup{GroupType: common.LdapGroupType, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"})
{GroupName: "test_group", GroupType: 1, 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 { type fields struct {
user *models.User user *models.User
pm promgr.ProjectManager pm promgr.ProjectManager

View File

@ -35,20 +35,14 @@ const googleEndpoint = "https://accounts.google.com"
type providerHelper struct { type providerHelper struct {
sync.Mutex sync.Mutex
ep endpoint
instance atomic.Value instance atomic.Value
setting atomic.Value setting atomic.Value
} creationTime time.Time
type endpoint struct {
url string
VerifyCert bool
} }
func (p *providerHelper) get() (*gooidc.Provider, error) { func (p *providerHelper) get() (*gooidc.Provider, error) {
if p.instance.Load() != nil { if p.instance.Load() != nil {
s := p.setting.Load().(models.OIDCSetting) if time.Now().Sub(p.creationTime) > 3*time.Second {
if s.Endpoint != p.ep.url || s.VerifyCert != p.ep.VerifyCert { // relevant settings have changed, need to re-create provider.
if err := p.create(); err != nil { if err := p.create(); err != nil {
return nil, err return nil, err
} }
@ -57,7 +51,7 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
if p.instance.Load() == nil { if p.instance.Load() == nil {
if err := p.reload(); err != nil { if err := p.reloadSetting(); err != nil {
return nil, err return nil, err
} }
if err := p.create(); err != nil { if err := p.create(); err != nil {
@ -65,7 +59,7 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
} }
go func() { go func() {
for { for {
if err := p.reload(); err != nil { if err := p.reloadSetting(); err != nil {
log.Warningf("Failed to refresh configuration, error: %v", err) log.Warningf("Failed to refresh configuration, error: %v", err)
} }
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
@ -73,10 +67,11 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
}() }()
} }
} }
return p.instance.Load().(*gooidc.Provider), nil return p.instance.Load().(*gooidc.Provider), nil
} }
func (p *providerHelper) reload() error { func (p *providerHelper) reloadSetting() error {
conf, err := config.OIDCSetting() conf, err := config.OIDCSetting()
if err != nil { if err != nil {
return fmt.Errorf("failed to load OIDC setting: %v", err) 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) return fmt.Errorf("failed to create OIDC provider, error: %v", err)
} }
p.instance.Store(provider) p.instance.Store(provider)
p.ep = endpoint{ p.creationTime = time.Now()
url: s.Endpoint,
VerifyCert: s.VerifyCert,
}
return nil return nil
} }

View File

@ -49,21 +49,20 @@ func TestMain(m *testing.M) {
func TestHelperLoadConf(t *testing.T) { func TestHelperLoadConf(t *testing.T) {
testP := &providerHelper{} testP := &providerHelper{}
assert.Nil(t, testP.setting.Load()) assert.Nil(t, testP.setting.Load())
err := testP.reload() err := testP.reloadSetting()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "test", testP.setting.Load().(models.OIDCSetting).Name) assert.Equal(t, "test", testP.setting.Load().(models.OIDCSetting).Name)
assert.Equal(t, endpoint{}, testP.ep)
} }
func TestHelperCreate(t *testing.T) { func TestHelperCreate(t *testing.T) {
testP := &providerHelper{} testP := &providerHelper{}
err := testP.reload() err := testP.reloadSetting()
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, testP.instance.Load()) assert.Nil(t, testP.instance.Load())
err = testP.create() err = testP.create()
assert.Nil(t, err) assert.Nil(t, err)
assert.EqualValues(t, "https://accounts.google.com", testP.ep.url)
assert.NotNil(t, testP.instance.Load()) assert.NotNil(t, testP.instance.Load())
assert.True(t, time.Now().Sub(testP.creationTime) < 2*time.Second)
} }
func TestHelperGet(t *testing.T) { func TestHelperGet(t *testing.T) {

View 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
}

View 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
}

View File

@ -22,8 +22,6 @@ import (
"net/url" "net/url"
"strings" "strings"
// "time"
commonhttp "github.com/goharbor/harbor/src/common/http" commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
) )
@ -130,9 +128,18 @@ func (r *Registry) Catalog() ([]string, error) {
return repos, nil return repos, nil
} }
// Ping ... // Ping checks by Head method
func (r *Registry) Ping() error { 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 { if err != nil {
return err return err
} }

View File

@ -211,7 +211,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) (
defer resp.Body.Close() 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")) digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
return return
} }

View File

@ -144,6 +144,7 @@ func init() {
beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog") 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/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/CVEWhitelist", &SysCVEWhitelistAPI{}, "get:Get;put:Put")
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List") beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete") beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")

View File

@ -212,10 +212,10 @@ func jobserviceHealthChecker() health.Checker {
} }
func registryHealthChecker() health.Checker { func registryHealthChecker() health.Checker {
url := getRegistryURL() + "/v2" url := getRegistryURL() + "/"
timeout := 60 * time.Second timeout := 60 * time.Second
period := 10 * 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) return PeriodicHealthChecker(checker, period)
} }

View File

@ -158,6 +158,7 @@ func (p *ProjectAPI) Post() {
if _, ok := pro.Metadata[models.ProMetaPublic]; !ok { if _, ok := pro.Metadata[models.ProMetaPublic]; !ok {
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false) pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false)
} }
// populate
owner := p.SecurityCtx.GetUsername() owner := p.SecurityCtx.GetUsername()
// set the owner as the system admin when the API being called by replication // set the owner as the system admin when the API being called by replication
@ -461,6 +462,7 @@ func (p *ProjectAPI) Put() {
if err := p.ProjectMgr.Update(p.project.ProjectID, if err := p.ProjectMgr.Update(p.project.ProjectID,
&models.Project{ &models.Project{
Metadata: req.Metadata, Metadata: req.Metadata,
CVEWhitelist: req.CVEWhitelist,
}); err != nil { }); err != nil {
p.ParseAndHandleError(fmt.Sprintf("failed to update project %d", p.ParseAndHandleError(fmt.Sprintf("failed to update project %d",
p.project.ProjectID), err) p.project.ProjectID), err)

View File

@ -52,6 +52,15 @@ func TestProjectMemberAPI_Get(t *testing.T) {
}, },
code: http.StatusBadRequest, code: http.StatusBadRequest,
}, },
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("/api/projects/1/members/%d", projAdminPMID),
credential: admin,
},
code: http.StatusOK,
},
// 404 // 404
{ {
request: &testingRequest{ request: &testingRequest{

View File

@ -49,6 +49,7 @@ func (t *RegistryAPI) Ping() {
ID *int64 `json:"id"` ID *int64 `json:"id"`
Type *string `json:"type"` Type *string `json:"type"`
URL *string `json:"url"` URL *string `json:"url"`
Region *string `json:"region"`
CredentialType *string `json:"credential_type"` CredentialType *string `json:"credential_type"`
AccessKey *string `json:"access_key"` AccessKey *string `json:"access_key"`
AccessSecret *string `json:"access_secret"` AccessSecret *string `json:"access_secret"`

View File

@ -17,6 +17,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/goharbor/harbor/src/pkg/scan"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort" "sort"
@ -34,7 +35,6 @@ import (
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils" "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/log"
"github.com/goharbor/harbor/src/common/utils/notary" "github.com/goharbor/harbor/src/common/utils/notary"
"github.com/goharbor/harbor/src/common/utils/registry" "github.com/goharbor/harbor/src/common/utils/registry"
@ -332,7 +332,7 @@ func (ra *RepositoryAPI) Delete() {
go func(tag string) { go func(tag string) {
e := &event.Event{ e := &event.Event{
Type: event.EventTypeImagePush, Type: event.EventTypeImageDelete,
Resource: &model.Resource{ Resource: &model.Resource{
Type: model.ResourceTypeImage, Type: model.ResourceTypeImage,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
@ -1036,21 +1036,9 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername())) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
res := []*models.VulnerabilityItem{} res, err := scan.VulnListByDigest(digest)
overview, err := dao.GetImgScanOverview(digest)
if err != nil { if err != nil {
ra.SendInternalServerError(fmt.Errorf("failed to get the scan overview, error: %v", err)) log.Errorf("Failed to get vulnerability list for image: %s:%s", repository, tag)
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)
} }
ra.Data["json"] = res ra.Data["json"] = res
ra.ServeJSON() ra.ServeJSON()

View 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
}
}

View 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...)
}

View File

@ -24,7 +24,6 @@ import (
commonhttp "github.com/goharbor/harbor/src/common/http" commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils" "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/log"
"github.com/goharbor/harbor/src/common/utils/registry" "github.com/goharbor/harbor/src/common/utils/registry"
"github.com/goharbor/harbor/src/common/utils/registry/auth" "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 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
}

View File

@ -20,11 +20,11 @@ import (
"strings" "strings"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/group"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
goldap "gopkg.in/ldap.v2" 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" "github.com/goharbor/harbor/src/common/models"
ldapUtils "github.com/goharbor/harbor/src/common/utils/ldap" ldapUtils "github.com/goharbor/harbor/src/common/utils/ldap"
"github.com/goharbor/harbor/src/common/utils/log" "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.Username = ldapUsers[0].Username
u.Email = strings.TrimSpace(ldapUsers[0].Email) u.Email = strings.TrimSpace(ldapUsers[0].Email)
u.Realname = ldapUsers[0].Realname u.Realname = ldapUsers[0].Realname
userGroups := make([]*models.UserGroup, 0) ugIDs := []int{}
dn := ldapUsers[0].DN dn := ldapUsers[0].DN
if err = ldapSession.Bind(dn, m.Password); err != nil { 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 { for _, groupDN := range ldapUsers[0].GroupDNList {
groupDN = utils.TrimLower(groupDN) groupDN = utils.TrimLower(groupDN)
// Attach LDAP group admin
if len(groupAdminDN) > 0 && groupAdminDN == groupDN { if len(groupAdminDN) > 0 && groupAdminDN == groupDN {
u.HasAdminRole = true u.HasAdminRole = true
} }
@ -103,16 +104,16 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
GroupType: 1, GroupType: 1,
LdapGroupDN: groupDN, LdapGroupDN: groupDN,
} }
userGroupList, err := group.QueryUserGroup(userGroupQuery) userGroups, err := group.QueryUserGroup(userGroupQuery)
if err != nil { if err != nil {
continue continue
} }
if len(userGroupList) == 0 { if len(userGroups) == 0 {
continue continue
} }
userGroups = append(userGroups, userGroupList[0]) ugIDs = append(ugIDs, userGroups[0].ID)
} }
u.GroupList = userGroups u.GroupIDs = ugIDs
return &u, nil return &u, nil
} }

View File

@ -229,8 +229,10 @@ type oidcCliReqCtxModifier struct{}
func (oc *oidcCliReqCtxModifier) Modify(ctx *beegoctx.Context) bool { func (oc *oidcCliReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
path := ctx.Request.URL.Path path := ctx.Request.URL.Path
if path != "/service/token" && !strings.HasPrefix(path, "/chartrepo/") { if path != "/service/token" &&
log.Debug("OIDC CLI modifer only handles request by docker CLI or helm CLI") !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 return false
} }
if ctx.Request.Context().Value(AuthModeKey).(string) != common.OIDCAuth { if ctx.Request.Context().Value(AuthModeKey).(string) != common.OIDCAuth {

View File

@ -20,7 +20,6 @@ import (
"time" "time"
"github.com/goharbor/harbor/src/common/dao" "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/models"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
errutil "github.com/goharbor/harbor/src/common/utils/error" 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) { func (d *driver) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
var total int64 var total int64
var projects []*models.Project var projects []*models.Project
var groupDNCondition string var groupIDs []int
// List with LDAP group projects
if query != nil && query.Member != nil { if query != nil && query.Member != nil {
groupDNCondition = group.GetGroupDNQueryCondition(query.Member.GroupList) groupIDs = query.Member.GroupIDs
} }
count, err := dao.GetTotalGroupProjects(groupIDs, query)
count, err := dao.GetTotalGroupProjects(groupDNCondition, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
total = int64(count) total = int64(count)
projects, err = dao.GetGroupProjects(groupDNCondition, query) projects, err = dao.GetGroupProjects(groupIDs, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -16,6 +16,7 @@ package promgr
import ( import (
"fmt" "fmt"
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
"strconv" "strconv"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
@ -44,6 +45,7 @@ type defaultProjectManager struct {
pmsDriver pmsdriver.PMSDriver pmsDriver pmsdriver.PMSDriver
metaMgrEnabled bool // if metaMgrEnabled is enabled, metaMgr will be used to CURD metadata metaMgrEnabled bool // if metaMgrEnabled is enabled, metaMgr will be used to CURD metadata
metaMgr metamgr.ProjectMetadataManager metaMgr metamgr.ProjectMetadataManager
whitelistMgr whitelist.Manager
} }
// NewDefaultProjectManager returns an instance of defaultProjectManager, // NewDefaultProjectManager returns an instance of defaultProjectManager,
@ -56,6 +58,7 @@ func NewDefaultProjectManager(driver pmsdriver.PMSDriver, metaMgrEnabled bool) P
} }
if metaMgrEnabled { if metaMgrEnabled {
mgr.metaMgr = metamgr.NewDefaultProjectMetadataManager() mgr.metaMgr = metamgr.NewDefaultProjectMetadataManager()
mgr.whitelistMgr = whitelist.NewDefaultManager()
} }
return mgr return mgr
} }
@ -77,6 +80,11 @@ func (d *defaultProjectManager) Get(projectIDOrName interface{}) (*models.Projec
for k, v := range meta { for k, v := range meta {
project.Metadata[k] = v project.Metadata[k] = v
} }
wl, err := d.whitelistMgr.Get(project.ProjectID)
if err != nil {
return nil, err
}
project.CVEWhitelist = *wl
} }
return project, nil return project, nil
} }
@ -85,11 +93,14 @@ func (d *defaultProjectManager) Create(project *models.Project) (int64, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
if len(project.Metadata) > 0 && d.metaMgrEnabled { if d.metaMgrEnabled {
d.whitelistMgr.CreateEmpty(project.ProjectID)
if len(project.Metadata) > 0 {
if err = d.metaMgr.Add(id, project.Metadata); err != nil { if err = d.metaMgr.Add(id, project.Metadata); err != nil {
log.Errorf("failed to add metadata for project %s: %v", project.Name, err) log.Errorf("failed to add metadata for project %s: %v", project.Name, err)
} }
} }
}
return id, nil return id, nil
} }
@ -110,7 +121,6 @@ func (d *defaultProjectManager) Delete(projectIDOrName interface{}) error {
} }
func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *models.Project) error { func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *models.Project) error {
if len(project.Metadata) > 0 && d.metaMgrEnabled {
pro, err := d.Get(projectIDOrName) pro, err := d.Get(projectIDOrName)
if err != nil { if err != nil {
return err return err
@ -118,8 +128,12 @@ func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *mod
if pro == nil { if pro == nil {
return fmt.Errorf("project %v not found", projectIDOrName) return fmt.Errorf("project %v not found", projectIDOrName)
} }
// TODO transaction? // TODO transaction?
if d.metaMgrEnabled {
if err := d.whitelistMgr.Set(pro.ProjectID, project.CVEWhitelist); err != nil {
return err
}
if len(project.Metadata) > 0 {
metaNeedUpdated := map[string]string{} metaNeedUpdated := map[string]string{}
metaNeedCreated := map[string]string{} metaNeedCreated := map[string]string{}
if pro.Metadata == nil { if pro.Metadata == nil {
@ -140,7 +154,7 @@ func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *mod
return err return err
} }
} }
}
return d.pmsDriver.Update(projectIDOrName, project) return d.pmsDriver.Update(projectIDOrName, project)
} }
@ -179,6 +193,7 @@ func (d *defaultProjectManager) List(query *models.ProjectQueryParam) (*models.P
project.Metadata = meta project.Metadata = meta
} }
} }
// the whitelist is not populated deliberately
return result, nil return result, nil
} }

View File

@ -169,6 +169,7 @@ func TestPMSPolicyChecker(t *testing.T) {
models.ProMetaEnableContentTrust: "true", models.ProMetaEnableContentTrust: "true",
models.ProMetaPreventVul: "true", models.ProMetaPreventVul: "true",
models.ProMetaSeverity: "low", models.ProMetaSeverity: "low",
models.ProMetaReuseSysCVEWhitelist: "false",
}, },
}) })
require.Nil(t, err) require.Nil(t, err)
@ -180,9 +181,10 @@ func TestPMSPolicyChecker(t *testing.T) {
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low") contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low")
assert.True(t, contentTrustFlag) 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.True(t, projectVulnerableEnabled)
assert.Equal(t, projectVulnerableSeverity, models.SevLow) assert.Equal(t, projectVulnerableSeverity, models.SevLow)
assert.Empty(t, wl.Items)
} }
func TestMatchNotaryDigest(t *testing.T) { func TestMatchNotaryDigest(t *testing.T) {

View File

@ -2,7 +2,6 @@ package proxy
import ( import (
"encoding/json" "encoding/json"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/clair" "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/config"
"github.com/goharbor/harbor/src/core/promgr" "github.com/goharbor/harbor/src/core/promgr"
coreutils "github.com/goharbor/harbor/src/core/utils" coreutils "github.com/goharbor/harbor/src/core/utils"
"github.com/goharbor/harbor/src/pkg/scan"
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
"context" "context"
"fmt" "fmt"
@ -82,7 +83,7 @@ type policyChecker interface {
// contentTrustEnabled returns whether a project has enabled content trust. // contentTrustEnabled returns whether a project has enabled content trust.
contentTrustEnabled(name string) bool contentTrustEnabled(name string) bool
// vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity. // 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 { type pmsPolicyChecker struct {
@ -97,13 +98,28 @@ func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool {
} }
return project.ContentTrustEnabled() 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) project, err := pc.pm.Get(name)
wl := models.CVEWhitelist{}
if err != nil { if err != nil {
log.Errorf("Unexpected error when getting the project, error: %v", err) 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 // 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) vh.next.ServeHTTP(rw, req)
return return
} }
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName) projectVulnerableEnabled, projectVulnerableSeverity, wl := getPolicyChecker().vulnerablePolicy(img.projectName)
if !projectVulnerableEnabled { if !projectVulnerableEnabled {
vh.next.ServeHTTP(rw, req) vh.next.ServeHTTP(rw, req)
return return
} }
overview, err := dao.GetImgScanOverview(img.digest) vl, err := scan.VulnListByDigest(img.digest)
if err != nil { 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) log.Errorf("Failed to get the vulnerability list, error: %v", err)
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get ImgScanOverview."), http.StatusPreconditionFailed) http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get vulnerabilities."), http.StatusPreconditionFailed)
return return
} }
// severity is 0 means that the image fails to scan or not scanned successfully. filtered := vl.ApplyWhitelist(wl)
if overview == nil || overview.Sev == 0 { msg := vh.filterMsg(img, filtered)
log.Debugf("cannot get the image scan overview info, failing the response.") log.Info(msg)
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Cannot get the image severity."), http.StatusPreconditionFailed) if int(vl.Severity()) >= int(projectVulnerableSeverity) {
return 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)
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)
return return
} }
vh.next.ServeHTTP(rw, req) 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) { func matchNotaryDigest(img imageInfo) (bool, error) {
if NotaryEndpoint == "" { if NotaryEndpoint == "" {
NotaryEndpoint = config.InternalNotaryEndpoint() NotaryEndpoint = config.InternalNotaryEndpoint()

View File

@ -38,7 +38,15 @@ func Init(urls ...string) error {
return err return err
} }
Proxy = httputil.NewSingleHostReverseProxy(targetURL) 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 return nil
} }

View File

@ -96,6 +96,7 @@ func initRouters() {
beego.Router("/api/system/gc/:id([0-9]+)/log", &api.GCAPI{}, "get:GetLog") 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/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/system/CVEWhitelist", &api.SysCVEWhitelistAPI{}, "get:Get;put:Put")
beego.Router("/api/logs", &api.LogAPI{}) beego.Router("/api/logs", &api.LogAPI{})

View File

@ -10,6 +10,7 @@ require (
github.com/Unknwon/goconfig v0.0.0-20160216183935-5f601ca6ef4d // indirect github.com/Unknwon/goconfig v0.0.0-20160216183935-5f601ca6ef4d // indirect
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/astaxie/beego v1.9.0 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/beego/i18n v0.0.0-20140604031826-e87155e8f0c0
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect

View File

@ -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/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 h1:tPzS+D1oCLi+SEb/TLNRNYpCjaMVfAGoy9OTLwS5ul4=
github.com/astaxie/beego v1.9.0/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U= 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 h1:fQaDnUQvBXHHQdGBu9hz8nPznB4BeiPQokvmQVjmNEw=
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0/go.mod h1:KLeFCpAMq2+50NkXC8iiJxLLiiTfTqrGtKEVm+2fk7s= 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= 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/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 h1:6WV8LvwPpDhKjo5U9O6b4+xdG/jTXNPwlDme/MTo8Ns=
github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= 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/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 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=

View File

@ -34,7 +34,7 @@ func RedisKeyScheduled(namespace string) string {
// RedisKeyLastPeriodicEnqueue returns key of timestamp if last periodic enqueue. // RedisKeyLastPeriodicEnqueue returns key of timestamp if last periodic enqueue.
func RedisKeyLastPeriodicEnqueue(namespace string) string { 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. // KeyNamespacePrefix returns the based key based on the namespace.

View File

@ -34,6 +34,12 @@ import (
_ "github.com/goharbor/harbor/src/replication/adapter/native" _ "github.com/goharbor/harbor/src/replication/adapter/native"
// register the Huawei adapter // register the Huawei adapter
_ "github.com/goharbor/harbor/src/replication/adapter/huawei" _ "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 // Replication implements the job interface

View File

@ -16,6 +16,12 @@ package period
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"testing"
"time"
"github.com/pkg/errors"
"github.com/goharbor/harbor/src/jobservice/common/rds" "github.com/goharbor/harbor/src/jobservice/common/rds"
"github.com/goharbor/harbor/src/jobservice/common/utils" "github.com/goharbor/harbor/src/jobservice/common/utils"
"github.com/goharbor/harbor/src/jobservice/env" "github.com/goharbor/harbor/src/jobservice/env"
@ -26,9 +32,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"sync"
"testing"
"time"
) )
// EnqueuerTestSuite tests functions of enqueuer // EnqueuerTestSuite tests functions of enqueuer
@ -89,19 +92,30 @@ func (suite *EnqueuerTestSuite) TestEnqueuer() {
suite.enqueuer.stopChan <- true suite.enqueuer.stopChan <- true
}() }()
<-time.After(1 * time.Second)
key := rds.RedisKeyScheduled(suite.namespace) key := rds.RedisKeyScheduled(suite.namespace)
conn := suite.pool.Get() conn := suite.pool.Get()
defer func() { defer func() {
_ = conn.Close() _ = conn.Close()
}() }()
tk := time.NewTicker(500 * time.Millisecond)
defer tk.Stop()
for {
select {
case <-tk.C:
count, err := redis.Int(conn.Do("ZCARD", key)) count, err := redis.Int(conn.Do("ZCARD", key))
require.Nil(suite.T(), err, "count scheduled: nil error expected but got %s", err) require.Nil(suite.T(), err, "count scheduled: nil error expected but got %s", err)
assert.Condition(suite.T(), func() bool { if assert.Condition(suite.T(), func() (success bool) {
return count > 0 return count > 0
}, "count of scheduled jobs should be greater than 0 but got %d", count) }, "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() err := suite.enqueuer.start()
@ -112,7 +126,7 @@ func (suite *EnqueuerTestSuite) prepare() {
now := time.Now() now := time.Now()
minute := now.Minute() minute := now.Minute()
coreSpec := fmt.Sprintf("30,50 %d * * * *", minute+2) coreSpec := fmt.Sprintf("0-59 %d * * * *", minute)
// Prepare one // Prepare one
p := &Policy{ p := &Policy{

136
src/pkg/scan/vuln.go Normal file
View 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
View 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)
}

View 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{}
}

View 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
}

View 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))
}
}
}

View File

@ -44,11 +44,15 @@
<div class="form-group"> <div class="form-group">
<label for="destination_url" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.URL' | <label for="destination_url" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.URL' |
translate }}</label> translate }}</label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (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"> [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" <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"> 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)"> <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 }} {{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
</span> </span>
</label> </label>
@ -57,15 +61,19 @@
<div class="form-group"> <div class="form-group">
<label for="destination_access_key" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_ID' | <label for="destination_access_key" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_ID' |
translate }}</label> 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"> [(ngModel)]="target.credential.access_key" size="28" name="access_key" #access_key="ngModel">
</div> </div>
<!-- access_secret --> <!-- access_secret -->
<div class="form-group"> <div class="form-group">
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_SECRET' | <label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_SECRET' |
translate }}</label> translate }}</label>
<input type="password" placeholder="Access Secret" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable" <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"> [(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> </div>
<!-- Verify Remote Cert --> <!-- Verify Remote Cert -->
<div class="form-group"> <div class="form-group">
@ -88,7 +96,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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> 'DESTINATION.TEST_CONNECTION' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' | <button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' |
translate }}</button> translate }}</button>

View File

@ -15,3 +15,6 @@
.inputWidth { .inputWidth {
width: 216px; width: 216px;
} }
.display-none {
display: none
}

View File

@ -32,6 +32,7 @@ import { Endpoint, PingEndpoint } from "../service/interface";
import { clone, compareValue, isEmptyObject } from "../utils"; import { clone, compareValue, isEmptyObject } from "../utils";
const FAKE_PASSWORD = "rjGcfuRu"; const FAKE_PASSWORD = "rjGcfuRu";
const FAKE_JSON_KEY = "No Change";
const DOCKERHUB_URL = "https://hub.docker.com"; const DOCKERHUB_URL = "https://hub.docker.com";
@Component({ @Component({
selector: "hbr-create-edit-endpoint", selector: "hbr-create-edit-endpoint",
@ -49,12 +50,13 @@ export class CreateEditEndpointComponent
closable: boolean = false; closable: boolean = false;
editable: boolean; editable: boolean;
adapterList: string[]; adapterList: string[];
endpointList: object[] = [];
target: Endpoint = this.initEndpoint(); target: Endpoint = this.initEndpoint();
selectedType: string; selectedType: string;
initVal: Endpoint; initVal: Endpoint;
targetForm: NgForm; targetForm: NgForm;
@ViewChild("targetForm") currentForm: NgForm; @ViewChild("targetForm") currentForm: NgForm;
targetEndpoint;
testOngoing: boolean; testOngoing: boolean;
onGoing: boolean; onGoing: boolean;
endpointId: number | string; endpointId: number | string;
@ -188,8 +190,8 @@ export class CreateEditEndpointComponent
this.urlDisabled = this.target.type === 'docker-hub' ? true : false; this.urlDisabled = this.target.type === 'docker-hub' ? true : false;
// Keep data cache // Keep data cache
this.initVal = clone(target); this.initVal = clone(target);
this.initVal.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 = FAKE_PASSWORD; this.target.credential.access_secret = this.target.type === 'google-gcr' ? FAKE_JSON_KEY : FAKE_PASSWORD;
// Open the modal now // Open the modal now
this.open(); this.open();
@ -219,6 +221,104 @@ export class CreateEditEndpointComponent
this.urlDisabled = false; this.urlDisabled = false;
this.targetForm.controls.endpointUrl.setValue(""); 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() { testConnection() {

View File

@ -62,8 +62,8 @@
<span class="spinner spinner-inline spinner-position" [hidden]="onGoing === false"></span> <span class="spinner spinner-inline spinner-position" [hidden]="onGoing === false"></span>
<div formArrayName="filters"> <div formArrayName="filters">
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index"> <div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
<div [formGroupName]="i"> <div [formGroupName]="i" *ngIf="supportedFilters[i]?.type !=='label' || (supportedFilters[i]?.type==='label' && supportedFilterLabels?.length)">
<div class="width-70"> <div class="width-70" >
<label>{{"REPLICATION." + supportedFilters[i]?.type.toUpperCase() | translate}}:</label> <label>{{"REPLICATION." + supportedFilters[i]?.type.toUpperCase() | translate}}:</label>
</div> </div>
<label *ngIf="supportedFilters[i]?.style==='input'" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" <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> <option *ngFor="let value of supportedFilters[i]?.values;" value="{{value}}">{{value}}</option>
</select> </select>
</div> </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"> <div class="resource-box" *ngIf="supportedFilters[i]?.style==='radio' && supportedFilters[i]?.values.length <= 1">
<span>{{supportedFilters[i]?.values}}</span> <span>{{supportedFilters[i]?.values}}</span>
</div> </div>
@ -85,6 +106,7 @@
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen> <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==='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==='tag'">{{'TOOLTIP.TAG_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> <span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='resource'">{{'TOOLTIP.RESOURCE_FILTER' | translate}}</span>
</clr-tooltip-content> </clr-tooltip-content>
</clr-tooltip> </clr-tooltip>

View File

@ -269,3 +269,28 @@ clr-modal {
.display-none{ .display-none{
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;
}

View File

@ -32,6 +32,7 @@ import { ErrorHandler } from "../error-handler/error-handler";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { EndpointService } from "../service/endpoint.service"; import { EndpointService } from "../service/endpoint.service";
import { cronRegex } from "../utils"; import { cronRegex } from "../utils";
import { FilterType } from "../shared/shared.const";
@Component({ @Component({
@ -67,6 +68,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
cronString: string; cronString: string;
supportedTriggers: string[]; supportedTriggers: string[];
supportedFilters: Filter[]; supportedFilters: Filter[];
supportedFilterLabels: { name: string; color: string; select: boolean; scope: string; }[] = [];
@Input() withAdmiral: boolean; @Input() withAdmiral: boolean;
@Output() goToRegistry = new EventEmitter<any>(); @Output() goToRegistry = new EventEmitter<any>();
@ -92,6 +95,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.supportedFilters = adapter.supported_resource_filters; this.supportedFilters = adapter.supported_resource_filters;
this.supportedFilters.forEach(element => { this.supportedFilters.forEach(element => {
this.filters.push(this.initFilter(element.type)); this.filters.push(this.initFilter(element.type));
// get supportedFilterLabels labels from supportedFilters
this.getLabelListFromAdapter(element);
}); });
this.supportedTriggers = adapter.supported_triggers; this.supportedTriggers = adapter.supported_triggers;
this.ruleForm.get("trigger").get("type").setValue(this.supportedTriggers[0]); 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; return this.ruleForm.get("filters") as FormArray;
} }
setFilter(filters: Filter[]) { 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); const filterFormArray = this.fb.array(filterFGs);
this.ruleForm.setControl("filters", filterFormArray); this.ruleForm.setControl("filters", filterFormArray);
} }
initFilter(name: string) { 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({ return this.fb.group({
type: name, type: name,
value: '' value: ''
@ -314,7 +336,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
let filters: any = copyRuleForm.filters; let filters: any = copyRuleForm.filters;
// remove the filters which user not set. // remove the filters which user not set.
for (let i = filters.length - 1; i >= 0; i--) { 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); copyRuleForm.filters.splice(i, 1);
} }
} }
@ -356,6 +379,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.inlineAlert.close(); this.inlineAlert.close();
this.noSelectedEndpoint = true; this.noSelectedEndpoint = true;
this.isRuleNameValid = true; this.isRuleNameValid = true;
this.supportedFilterLabels = [];
this.policyId = -1; this.policyId = -1;
this.createEditRuleOpened = true; this.createEditRuleOpened = true;
@ -373,7 +398,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.repService.getRegistryInfo(srcRegistryId) this.repService.getRegistryInfo(srcRegistryId)
.pipe(finalize(() => (this.onGoing = false))) .pipe(finalize(() => (this.onGoing = false)))
.subscribe(adapter => { .subscribe(adapter => {
this.setFilterAndTrigger(adapter); this.setFilterAndTrigger(adapter, ruleInfo);
this.updateRuleFormAndCopyUpdateForm(ruleInfo); this.updateRuleFormAndCopyUpdateForm(ruleInfo);
}, (error: any) => { }, (error: any) => {
this.inlineAlert.showInlineError(error); 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.supportedFilters = adapter.supported_resource_filters;
this.setFilter([]); this.setFilter([]);
this.supportedFilters.forEach(element => { this.supportedFilters.forEach(element => {
this.filters.push(this.initFilter(element.type)); 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.supportedTriggers = adapter.supported_triggers;
this.ruleForm.get("trigger").get("type").setValue(this.supportedTriggers[0]); 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 { close(): void {
this.createEditRuleOpened = false; this.createEditRuleOpened = false;
} }
@ -462,8 +533,12 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
} }
if (!findTag) { if (!findTag) {
if (this.supportedFilters[i].type === FilterType.LABEL) {
filtersArray.push({ type: this.supportedFilters[i].type, value: [] });
} else {
filtersArray.push({ type: this.supportedFilters[i].type, value: "" }); filtersArray.push({ type: this.supportedFilters[i].type, value: "" });
} }
}
} }
return filtersArray; return filtersArray;
@ -482,4 +557,20 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
} }
return trigger_settingsControls.controls.cron.touched || trigger_settingsControls.controls.cron.dirty; 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));
}
} }

View File

@ -130,7 +130,7 @@ export interface ReplicationRule extends Base {
export class Filter { export class Filter {
type: string; type: string;
value?: string; value?: any;
constructor(type: string) { constructor(type: string) {
this.type = type; this.type = type;
} }
@ -434,3 +434,8 @@ export interface HttpOptionTextInterface {
withCredentials?: boolean; withCredentials?: boolean;
} }
export interface ProjectRootInterface {
NAME: string;
VALUE: number;
LABEL: string;
}

View File

@ -61,6 +61,12 @@ export const CommonRoutes = {
export const enum ConfirmationState { export const enum ConfirmationState {
NA, CONFIRMED, CANCEL NA, CONFIRMED, CANCEL
} }
export const FilterType = {
NAME: "name",
TAG: "tag",
LABEL: "label",
RESOURCE: "resource"
};
export const enum ConfirmationButtons { export const enum ConfirmationButtons {
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE, REPLICATE_CANCEL, STOP_CANCEL 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': '#F52F52', 'textColor': 'black' }, { 'color': '#FF5501', 'textColor': 'black' },
{ 'color': '#F57600', 'textColor': 'black' }, { 'color': '#FFDC0B', '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
}

View File

@ -1,11 +1,11 @@
<div class="tip-wrapper tip-position" [style.width]='maxWidth'> <div class="tip-wrapper tip-position" [style.width]='maxWidth'>
<clr-tooltip> <clr-tooltip>
<div clrTooltipTrigger> <div clrTooltipTrigger class="tip-block">
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(5)'></div> <div class="tip-wrapper 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 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 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 bar-block-unknown" [style.width]='tipWidth(2)'></div>
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(1)'></div> <div class="tip-wrapper bar-block-none" [style.width]='tipWidth(1)'></div>
</div> </div>
<clr-tooltip-content [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen> <clr-tooltip-content [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen>
<div [ngSwitch]="scanLevel" class="bar-tooltip-font-larger"> <div [ngSwitch]="scanLevel" class="bar-tooltip-font-larger">

View File

@ -19,7 +19,7 @@ import { CookieService } from 'ngx-cookie';
import { AppConfig } from './app-config'; import { AppConfig } from './app-config';
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const'; import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
import { maintainUrlQueryParmas } from './shared/shared.utils'; 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 { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs"; import { Observable, throwError as observableThrowError } from "rxjs";
export const systemInfoEndpoint = "/api/systeminfo"; export const systemInfoEndpoint = "/api/systeminfo";
@ -67,7 +67,10 @@ export class AppConfigService {
} }
public isLdapMode(): boolean { 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 // Return the reconstructed admiral url

View File

@ -28,7 +28,7 @@
<clr-icon shape="users" clrVerticalNavIcon></clr-icon> <clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}} {{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
</a> </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> <clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}} {{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}}
</a> </a>

View File

@ -54,6 +54,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
searchSub: Subscription; searchSub: Subscription;
searchCloseSub: Subscription; searchCloseSub: Subscription;
isLdapMode: boolean;
isHttpAuthMode: boolean;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -63,6 +65,11 @@ export class HarborShellComponent implements OnInit, OnDestroy {
private appConfigService: AppConfigService) { } private appConfigService: AppConfigService) { }
ngOnInit() { ngOnInit() {
if (this.appConfigService.isLdapMode()) {
this.isLdapMode = true;
} else if (this.appConfigService.isHttpAuthMode()) {
this.isHttpAuthMode = true;
}
this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(searchEvt => { this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(searchEvt => {
if (searchEvt && searchEvt.trim() !== "") { if (searchEvt && searchEvt.trim() !== "") {
this.isSearchResultsOpened = true; this.isSearchResultsOpened = true;
@ -97,11 +104,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
return account != null && account.has_admin_role; 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 { public get isUserExisting(): boolean {
let account = this.session.getCurrentUser(); let account = this.session.getCurrentUser();
return account != null; return account != null;

View File

@ -1,11 +1,12 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false"> <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> <h3 class="modal-title" *ngIf="mode !== 'create'">{{'GROUP.EDIT' | translate}}</h3>
<div class="modal-body"> <div class="modal-body">
<form class="form" #groupForm="ngForm"> <form class="form" #groupForm="ngForm">
<section class="form-block"> <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" class="required">{{ 'GROUP.GROUP_DN' | translate}}</label>
<label for="ldap_group_dn" <label for="ldap_group_dn"
aria-haspopup="true" aria-haspopup="true"
@ -22,7 +23,7 @@
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group" *ngIf="isLdapMode">
<label for="type">{{'GROUP.TYPE' | translate}}</label> <label for="type">{{'GROUP.TYPE' | translate}}</label>
<label id="type">LDAP</label> <label id="type">LDAP</label>
</div> </div>

View File

@ -1,13 +1,15 @@
import {finalize} from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core"; import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms"; import { NgForm } from "@angular/forms";
import { GroupType } from "@harbor/ui";
import { GroupService } from "../group.service"; import { GroupService } from "../group.service";
import { MessageHandlerService } from "./../../shared/message-handler/message-handler.service"; import { MessageHandlerService } from "./../../shared/message-handler/message-handler.service";
import { SessionService } from "./../../shared/session.service"; import { SessionService } from "./../../shared/session.service";
import { UserGroup } from "./../group"; import { UserGroup } from "./../group";
import { AppConfigService } from "../../app-config.service";
@Component({ @Component({
selector: "hbr-add-group-modal", selector: "hbr-add-group-modal",
@ -19,7 +21,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
mode = "create"; mode = "create";
dnTooltip = 'TOOLTIP.ITEM_REQUIRED'; dnTooltip = 'TOOLTIP.ITEM_REQUIRED';
group: UserGroup = new UserGroup(); group: UserGroup;
formChangeSubscription: Subscription; formChangeSubscription: Subscription;
@ -30,14 +32,25 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
@Output() dataChange = new EventEmitter(); @Output() dataChange = new EventEmitter();
isLdapMode: boolean;
isHttpAuthMode: boolean;
constructor( constructor(
private session: SessionService, private session: SessionService,
private msgHandler: MessageHandlerService, private msgHandler: MessageHandlerService,
private appConfigService: AppConfigService,
private groupService: GroupService, private groupService: GroupService,
private cdr: ChangeDetectorRef 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() { } ngOnDestroy() { }
@ -108,7 +121,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
} }
resetGroup() { resetGroup() {
this.group = new UserGroup(); this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : GroupType.HTTP_TYPE);
this.groupForm.reset(); this.groupForm.reset();
} }
} }

View File

@ -15,18 +15,18 @@
<clr-icon shape="plus" size="15"></clr-icon>&nbsp;{{'GROUP.ADD' | translate}}</button> <clr-icon shape="plus" size="15"></clr-icon>&nbsp;{{'GROUP.ADD' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="editGroup()" [disabled]="!canEditGroup"> <button type="button" class="btn btn-sm btn-secondary" (click)="editGroup()" [disabled]="!canEditGroup">
<clr-icon shape="pencil" size="15"></clr-icon>&nbsp;{{'GROUP.EDIT' | translate}}</button> <clr-icon shape="pencil" size="15"></clr-icon>&nbsp;{{'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>&nbsp;{{'GROUP.DELETE' | translate}}</button> <clr-icon shape="times" size="15"></clr-icon>&nbsp;{{'GROUP.DELETE' | translate}}</button>
</clr-dg-action-bar> </clr-dg-action-bar>
<clr-dg-column>{{'GROUP.NAME' | translate}}</clr-dg-column> <clr-dg-column>{{'GROUP.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'GROUP.TYPE' | 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-row *clrDgItems="let group of groups" [clrDgItem]="group">
<clr-dg-cell>{{group.group_name}}</clr-dg-cell> <clr-dg-cell>{{group.group_name}}</clr-dg-cell>
<clr-dg-cell>{{groupToSring(group.group_type) | translate}}</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-row>
<clr-dg-footer> <clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="15"> <clr-dg-pagination #pagination [clrDgPageSize]="15">

View File

@ -4,7 +4,7 @@ import { flatMap, catchError } from "rxjs/operators";
import { SessionService } from "./../shared/session.service"; import { SessionService } from "./../shared/session.service";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { Component, OnInit, ViewChild, OnDestroy } from "@angular/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 { import {
ConfirmationTargets, ConfirmationTargets,
@ -19,6 +19,8 @@ import { UserGroup } from "./group";
import { GroupService } from "./group.service"; import { GroupService } from "./group.service";
import { MessageHandlerService } from "../shared/message-handler/message-handler.service"; import { MessageHandlerService } from "../shared/message-handler/message-handler.service";
import { throwError as observableThrowError } from "rxjs"; import { throwError as observableThrowError } from "rxjs";
import { AppConfigService } from '../app-config.service';
@Component({ @Component({
selector: "app-group", selector: "app-group",
templateUrl: "./group.component.html", templateUrl: "./group.component.html",
@ -35,6 +37,7 @@ export class GroupComponent implements OnInit, OnDestroy {
delSub: Subscription; delSub: Subscription;
batchOps = 'idle'; batchOps = 'idle';
batchInfos = new Map(); batchInfos = new Map();
isLdapMode: boolean;
@ViewChild(AddGroupModalComponent) newGroupModal: AddGroupModalComponent; @ViewChild(AddGroupModalComponent) newGroupModal: AddGroupModalComponent;
@ -46,10 +49,14 @@ export class GroupComponent implements OnInit, OnDestroy {
private msgHandler: MessageHandlerService, private msgHandler: MessageHandlerService,
private session: SessionService, private session: SessionService,
private translateService: TranslateService, private translateService: TranslateService,
private appConfigService: AppConfigService
) { } ) { }
ngOnInit() { ngOnInit() {
this.loadData(); this.loadData();
if (this.appConfigService.isLdapMode()) {
this.isLdapMode = true;
}
this.delSub = this.operateDialogService.confirmationConfirm$.subscribe( this.delSub = this.operateDialogService.confirmationConfirm$.subscribe(
message => { message => {
if ( if (
@ -150,7 +157,13 @@ export class GroupComponent implements OnInit, OnDestroy {
} }
groupToSring(type: number) { 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 { doFilter(groupName: string): void {
@ -162,6 +175,12 @@ export class GroupComponent implements OnInit, OnDestroy {
} }
get canEditGroup(): boolean { get canEditGroup(): boolean {
return (
this.selectedGroups.length === 1 &&
this.session.currentUser.has_admin_role && this.isLdapMode
);
}
get canDeleteGroup(): boolean {
return ( return (
this.selectedGroups.length === 1 && this.selectedGroups.length === 1 &&
this.session.currentUser.has_admin_role this.session.currentUser.has_admin_role

View File

@ -4,9 +4,9 @@ export class UserGroup {
group_type: number; group_type: number;
ldap_group_dn?: string; ldap_group_dn?: string;
constructor() { constructor(groupType) {
{ {
this.group_type = 1; this.group_type = groupType;
} }
} }
} }

View File

@ -30,7 +30,7 @@ export class AddGroupComponent implements OnInit {
currentTerm = ''; currentTerm = '';
selectedRole = 1; selectedRole = 1;
group = new UserGroup(); group = new UserGroup(1);
selectedGroups: UserGroup[] = []; selectedGroups: UserGroup[] = [];
groups: UserGroup[] = []; groups: UserGroup[] = [];
totalCount = 0; totalCount = 0;
@ -89,7 +89,7 @@ export class AddGroupComponent implements OnInit {
resetModaldata() { resetModaldata() {
this.createGroupMode = false; this.createGroupMode = false;
this.group = new UserGroup(); this.group = new UserGroup(1);
this.selectedRole = 1; this.selectedRole = 1;
this.selectedGroups = []; this.selectedGroups = [];
this.groups = []; this.groups = [];

View File

@ -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>

View File

@ -0,0 +1,8 @@
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
.padding-0 {
padding: 0;
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -16,7 +16,7 @@
<button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasCreateMemberPermission"> <button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasCreateMemberPermission">
<span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.USER' | translate }}</span> <span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.USER' | translate }}</span>
</button> </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>&nbsp;{{'MEMBER.LDAP_GROUP' | translate}}</span> <span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.LDAP_GROUP' | translate}}</span>
</button> </button>
<clr-dropdown id='member-action' [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-link" clrDropdownTrigger> <clr-dropdown id='member-action' [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-link" clrDropdownTrigger>
@ -53,4 +53,5 @@
</div> </div>
<add-member [projectId]="projectId" [memberList]="members" (added)="addedMember($event)"></add-member> <add-member [projectId]="projectId" [memberList]="members" (added)="addedMember($event)"></add-member>
<add-group [projectId]="projectId" [memberList]="members" (added)="addedGroup($event)"></add-group> <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> </div>

View File

@ -17,8 +17,10 @@ import { Component, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, Chang
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Subscription, forkJoin, Observable } from "rxjs"; import { Subscription, forkJoin, Observable } from "rxjs";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { operateChanges, OperateInfo, OperationService, OperationState, UserPermissionService, USERSTATICPERMISSION, ErrorHandler import {
, errorHandler as errorHandFn } from "@harbor/ui"; operateChanges, OperateInfo, OperationService, OperationState, UserPermissionService, USERSTATICPERMISSION, ErrorHandler
, errorHandler as errorHandFn
} from "@harbor/ui";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service"; import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from "../../shared/shared.const"; import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from "../../shared/shared.const";
@ -30,6 +32,7 @@ import { Project } from "../../project/project";
import { Member } from "./member"; import { Member } from "./member";
import { SessionUser } from "../../shared/session-user"; import { SessionUser } from "../../shared/session-user";
import { AddGroupComponent } from './add-group/add-group.component'; 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 { MemberService } from "./member.service";
import { AddMemberComponent } from "./add-member/add-member.component"; import { AddMemberComponent } from "./add-member/add-member.component";
import { AppConfigService } from "../../app-config.service"; import { AppConfigService } from "../../app-config.service";
@ -56,16 +59,18 @@ export class MemberComponent implements OnInit, OnDestroy {
isDelete = false; isDelete = false;
isChangeRole = false; isChangeRole = false;
loading = false; loading = false;
isLdapMode: boolean = false;
isChangingRole = false; isChangingRole = false;
batchChangeRoleInfos = {}; batchChangeRoleInfos = {};
isLdapMode: boolean;
isHttpAuthMode: boolean;
@ViewChild(AddMemberComponent) @ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent; addMemberComponent: AddMemberComponent;
@ViewChild(AddGroupComponent) @ViewChild(AddGroupComponent)
addGroupComponent: AddGroupComponent; addGroupComponent: AddGroupComponent;
@ViewChild(AddHttpAuthGroupComponent)
addHttpAuthGroupComponent: AddHttpAuthGroupComponent;
hasCreateMemberPermission: boolean; hasCreateMemberPermission: boolean;
hasUpdateMemberPermission: boolean; hasUpdateMemberPermission: boolean;
hasDeleteMemberPermission: boolean; hasDeleteMemberPermission: boolean;
@ -108,13 +113,15 @@ export class MemberComponent implements OnInit, OnDestroy {
// Get current user from registered resolver. // Get current user from registered resolver.
this.currentUser = this.session.getCurrentUser(); this.currentUser = this.session.getCurrentUser();
this.retrieve(this.projectId, ""); this.retrieve(this.projectId, "");
// get member permission rule
this.getMemberPermissionRule(this.projectId);
if (this.appConfigService.isLdapMode()) { if (this.appConfigService.isLdapMode()) {
this.isLdapMode = true; this.isLdapMode = true;
} }
// get member permission rule if (this.appConfigService.isHttpAuthMode()) {
this.getMemberPermissionRule(this.projectId); this.isHttpAuthMode = true;
}
} }
doSearch(searchMember: string) { doSearch(searchMember: string) {
this.searchMember = searchMember; this.searchMember = searchMember;
this.retrieve(this.projectId, this.searchMember); this.retrieve(this.projectId, this.searchMember);
@ -172,7 +179,11 @@ export class MemberComponent implements OnInit, OnDestroy {
// Add group // Add group
openAddGroupModal() { openAddGroupModal() {
if (this.isLdapMode) {
this.addGroupComponent.open(); this.addGroupComponent.open();
} else {
this.addHttpAuthGroupComponent.openAddMemberModal();
}
} }
addedGroup(result: boolean) { addedGroup(result: boolean) {
this.searchMember = ""; this.searchMember = "";

View File

@ -38,6 +38,7 @@ import { ProjectLabelComponent } from "../project/project-label/project-label.co
import { HelmChartModule } from './helm-chart/helm-chart.module'; import { HelmChartModule } from './helm-chart/helm-chart.module';
import { RobotAccountComponent } from './robot-account/robot-account.component'; import { RobotAccountComponent } from './robot-account/robot-account.component';
import { AddRobotComponent } from './robot-account/add-robot/add-robot.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({ @NgModule({
imports: [ imports: [
@ -59,7 +60,8 @@ import { AddRobotComponent } from './robot-account/add-robot/add-robot.component
ProjectLabelComponent, ProjectLabelComponent,
AddGroupComponent, AddGroupComponent,
RobotAccountComponent, RobotAccountComponent,
AddRobotComponent AddRobotComponent,
AddHttpAuthGroupComponent
], ],
exports: [ProjectComponent, ListProjectComponent], exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, MemberService, RobotService] providers: [ProjectRoutingResolver, MemberService, RobotService]

View File

@ -5,7 +5,7 @@
<div class="modal-body"> <div class="modal-body">
<form #robotForm="ngForm"> <form #robotForm="ngForm">
<section class="form-block"> <section class="form-block">
<div class="form-group"> <div class="form-group padding-left-120">
<label class="col-md-3 <label class="col-md-3
form-group-label-override required" for="robot_name"> form-group-label-override required" for="robot_name">
{{'ROBOT_ACCOUNT.NAME' | translate}} {{'ROBOT_ACCOUNT.NAME' | translate}}
@ -31,57 +31,56 @@
</label> </label>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span> <span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div> </div>
<div class="form-group"> <div class="form-group padding-left-120">
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' | <label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' |
translate}}</label> translate}}</label>
<input type="text" size="255" class="input-width" <input type="text" size="255" class="input-width"
[(ngModel)]="robot.description" [(ngModel)]="robot.description"
name="robot_desc" id="robot_desc"> name="robot_desc" id="robot_desc">
</div> </div>
<div class="form-group"> <div class="clr-row">
<div class="clr-col-3 permission">
<label class="col-md-3"> <label class="col-md-3">
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}} {{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
</label> </label>
<label class="clr-col-md-8 no-margin padding-left-0"> </div>
<clr-checkbox-wrapper> <div class="clr-col">
<input type="checkbox" clrCheckbox [checked]="true" <div class="form-group padding-left-120">
[(ngModel)]="robot.access.isPullImage" name="isPullImage" <label>{{'ROBOT_ACCOUNT.PERMISSIONS_IMAGE' | translate}}</label>
id="permission-pull" class="clr-checkbox"> <div class="radio-inline">
<label for="permission-pull" class="clr-control-label"> <input type="radio" name="image-permission"
{{'ROBOT_ACCOUNT.PULL_PERMISSION' | translate}} id="image-permission-pull"
</label> value="pull"
</clr-checkbox-wrapper> [(ngModel)]="imagePermission">
</label> <label for="image-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
<label class="clr-col-md-8 no-margin padding-left-0"> </div>
<clr-checkbox-wrapper> <div class="radio-inline">
<input type="checkbox" clrCheckbox [checked]="true" <input type="radio" name="image-permission"
[(ngModel)]="robot.access.isPushOrPullImage" name="isPushOrPullImage" id="image-permission-push-and-pull"
id="permission-push" class="clr-checkbox"> value="push-and-pull"
<label for="permission-push" class="clr-control-label"> [(ngModel)]="imagePermission">
{{'ROBOT_ACCOUNT.PULL_PUSH_PERMISSION' | translate}} <label for="image-permission-push-and-pull">{{'ROBOT_ACCOUNT.PUSH' | translate}}
</label> & {{'ROBOT_ACCOUNT.PULL' | translate}}</label>
</clr-checkbox-wrapper> </div>
</label> </div>
<label class="clr-col-md-8 no-margin padding-left-0"> <div class="form-group padding-left-120">
<clr-checkbox-wrapper> <label>{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</label>
<input type="checkbox" clrCheckbox [checked]="true" <div class="checkbox-inline">
[(ngModel)]="robot.access.isPushChart" name="isPushChart" <input type="checkbox" id="helm-permission-push"
id="permission-push-chart" class="clr-checkbox"> [checked]="robot.access.isPushChart"
<label for="permission-push-chart" class="clr-control-label"> [(ngModel)]="robot.access.isPushChart"
{{'ROBOT_ACCOUNT.PUSH_CHART_PERMISSION' | translate}} name="helm-permission">
</label> <label for="helm-permission-push">{{'ROBOT_ACCOUNT.PUSH' | translate}}</label>
</clr-checkbox-wrapper> </div>
</label> <div class="checkbox-inline">
<label class="clr-col-md-8 no-margin padding-left-0"> <input type="checkbox" id="helm-permission-pull"
<clr-checkbox-wrapper> [checked]="robot.access.isPullChart"
<input type="checkbox" clrCheckbox [checked]="true" [(ngModel)]="robot.access.isPullChart"
[(ngModel)]="robot.access.isPullChart" name="isPullChart" name="helm-permission">
id="permission-pull-chart" class="clr-checkbox"> <label for="helm-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
<label for="permission-pull-chart" class="clr-control-label"> </div>
{{'ROBOT_ACCOUNT.PULL_CHART_PERMISSION' | translate}} </div>
</label> </div>
</clr-checkbox-wrapper>
</label>
</div> </div>
</section> </section>
</form> </form>

View File

@ -3,7 +3,7 @@
} }
.input-width { .input-width {
width: 200px; width: 300px;
} }
.copy-token { .copy-token {
@ -35,3 +35,12 @@
.no-margin { .no-margin {
margin: 0; margin: 0;
} }
.permission{
padding-top: 5px;
color: #000000;
}
.padding-left-120{
padding-left: 120px;
}

View File

@ -38,6 +38,7 @@ export class AddRobotComponent implements OnInit, OnDestroy {
robotNameChecker: Subject<string> = new Subject<string>(); robotNameChecker: Subject<string> = new Subject<string>();
nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME"; nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
robotForm: NgForm; robotForm: NgForm;
imagePermission: string = "push-and-pull";
@Input() projectId: number; @Input() projectId: number;
@Input() projectName: string; @Input() projectName: string;
@Output() create = new EventEmitter<boolean>(); @Output() create = new EventEmitter<boolean>();
@ -116,6 +117,14 @@ export class AddRobotComponent implements OnInit, OnDestroy {
if (this.isSubmitOnGoing) { if (this.isSubmitOnGoing) {
return; 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.isSubmitOnGoing = true;
this.robotService this.robotService
.addRobotAccount( .addRobotAccount(

View File

@ -16,7 +16,7 @@ export class Robot {
constructor () { constructor () {
this.access = <any>{}; this.access = <any>{};
// this.access[0].action = true; // this.access[0].action = true;
this.access.isPullImage = true; this.access.isPullImage = false;
this.access.isPushOrPullImage = true; this.access.isPushOrPullImage = true;
this.access.isPushChart = false; this.access.isPushChart = false;
this.access.isPullChart = false; this.access.isPullChart = false;

View File

@ -58,6 +58,7 @@
"TOOLTIP": { "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.", "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.", "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.", "RESOURCE_FILTER": "Filter the type of resources.",
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.", "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.", "PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
@ -310,10 +311,10 @@
"ENABLE_ACCOUNT": "Enable Account", "ENABLE_ACCOUNT": "Enable Account",
"DELETE": "Delete", "DELETE": "Delete",
"CREAT_ROBOT_ACCOUNT": "Creat Robot Account", "CREAT_ROBOT_ACCOUNT": "Creat Robot Account",
"PULL_PERMISSION": "Image pull", "PERMISSIONS_IMAGE": "Image",
"PULL_PUSH_PERMISSION": "Image pull / push", "PERMISSIONS_HELMCHART": "Helm Chart",
"PUSH_CHART_PERMISSION": "Helm chart push", "PUSH": "Push",
"PULL_CHART_PERMISSION": "Helm chart pull", "PULL": "Pull",
"FILTER_PLACEHOLDER": "Filter Robot Accounts", "FILTER_PLACEHOLDER": "Filter Robot Accounts",
"ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.", "ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.",
"ACCOUNT_EXISTING": "Robot Account is already exists.", "ACCOUNT_EXISTING": "Robot Account is already exists.",
@ -327,6 +328,7 @@
"GROUP": "Group", "GROUP": "Group",
"GROUPS": "Groups", "GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group", "IMPORT_LDAP_GROUP": "Import LDAP Group",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "New Group", "ADD": "New Group",
"EDIT": "Edit", "EDIT": "Edit",
"DELETE": "Delete", "DELETE": "Delete",
@ -339,8 +341,17 @@
"ADD_GROUP_SUCCESS": "Add group success", "ADD_GROUP_SUCCESS": "Add group success",
"EDIT_GROUP_SUCCESS": "Edit group success", "EDIT_GROUP_SUCCESS": "Edit group success",
"LDAP_TYPE": "LDAP", "LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "of", "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": { "AUDIT_LOG": {
"USERNAME": "Username", "USERNAME": "Username",

View File

@ -58,6 +58,7 @@
"TOOLTIP": { "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.", "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.", "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.", "RESOURCE_FILTER": "Filter the type of resources.",
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.", "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.", "PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
@ -311,10 +312,10 @@
"ENABLE_ACCOUNT": "Enable Account", "ENABLE_ACCOUNT": "Enable Account",
"DELETE": "Delete", "DELETE": "Delete",
"CREAT_ROBOT_ACCOUNT": "Creat Robot Account", "CREAT_ROBOT_ACCOUNT": "Creat Robot Account",
"PULL_PERMISSION": "Image pull", "PERMISSIONS_IMAGE": "Image",
"PULL_PUSH_PERMISSION": "Image pull / push", "PERMISSIONS_HELMCHART": "Helm Chart",
"PUSH_CHART_PERMISSION": "Helm chart push", "PUSH": "Push",
"PULL_CHART_PERMISSION": "Helm chart pull", "PULL": "Pull",
"FILTER_PLACEHOLDER": "Filter Robot Accounts", "FILTER_PLACEHOLDER": "Filter Robot Accounts",
"ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.", "ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.",
"ACCOUNT_EXISTING": "Robot Account is already exists.", "ACCOUNT_EXISTING": "Robot Account is already exists.",
@ -328,6 +329,7 @@
"GROUP": "Group", "GROUP": "Group",
"GROUPS": "Groups", "GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group", "IMPORT_LDAP_GROUP": "Import LDAP Group",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "Add", "ADD": "Add",
"EDIT": "Edit", "EDIT": "Edit",
"DELETE": "Delete", "DELETE": "Delete",
@ -339,8 +341,17 @@
"ADD_GROUP_SUCCESS": "Add group success", "ADD_GROUP_SUCCESS": "Add group success",
"EDIT_GROUP_SUCCESS": "Edit group success", "EDIT_GROUP_SUCCESS": "Edit group success",
"LDAP_TYPE": "LDAP", "LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "of", "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": { "AUDIT_LOG": {
"USERNAME": "Nombre de usuario", "USERNAME": "Nombre de usuario",

View File

@ -55,6 +55,7 @@
"TOOLTIP": { "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.", "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.", "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.", "RESOURCE_FILTER": "Filter the type of resources.",
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.", "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.", "PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
@ -302,10 +303,11 @@
"ENABLE_ACCOUNT": "permettre à compte ", "ENABLE_ACCOUNT": "permettre à compte ",
"DELETE": "Supprimer", "DELETE": "Supprimer",
"CREAT_ROBOT_ACCOUNT": "créat robot compte ", "CREAT_ROBOT_ACCOUNT": "créat robot compte ",
"PULL_PERMISSION": "Image pull", "PERMISSIONS_IMAGE": "Image",
"PULL_PUSH_PERMISSION": "Image pull / push", "PERMISSIONS_HELMCHART": "Helm Chart",
"PUSH_CHART_PERMISSION": "Helm chart push", "PUSH": "Push",
"PULL_CHART_PERMISSION": "Helm chart pull", "PULL": "Pull",
"FILTER_PLACEHOLDER": "Filter Robot Accounts", "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.", "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à.", "ACCOUNT_EXISTING": "le robot est existe déjà.",
@ -319,6 +321,7 @@
"Group": "Group", "Group": "Group",
"GROUPS": "Groups", "GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group", "IMPORT_LDAP_GROUP": "Import LDAP Group",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "Add", "ADD": "Add",
"EDIT": "Edit", "EDIT": "Edit",
"DELETE": "Delete", "DELETE": "Delete",
@ -331,8 +334,17 @@
"ADD_GROUP_SUCCESS": "Add group success", "ADD_GROUP_SUCCESS": "Add group success",
"EDIT_GROUP_SUCCESS": "Edit group success", "EDIT_GROUP_SUCCESS": "Edit group success",
"LDAP_TYPE": "LDAP", "LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "of", "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": { "AUDIT_LOG": {
"USERNAME": "Nom d'utilisateur", "USERNAME": "Nom d'utilisateur",

View File

@ -58,6 +58,7 @@
"TOOLTIP": { "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.", "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.", "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.", "RESOURCE_FILTER": "Filter the type of resources.",
"PUSH_BASED": "Push the resources from the local Harbor to the remote registry.", "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.", "PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
@ -308,10 +309,10 @@
"ENABLE_ACCOUNT": "Ativar conta", "ENABLE_ACCOUNT": "Ativar conta",
"DELETE": "Remover", "DELETE": "Remover",
"CREAT_ROBOT_ACCOUNT": "CRIA robô conta", "CREAT_ROBOT_ACCOUNT": "CRIA robô conta",
"PULL_PERMISSION": "Image pull", "PERMISSIONS_IMAGE": "Image",
"PULL_PUSH_PERMISSION": "Image pull / push", "PERMISSIONS_HELMCHART": "Helm Chart",
"PUSH_CHART_PERMISSION": "Helm chart push", "PUSH": "Push",
"PULL_CHART_PERMISSION": "Helm chart pull", "PULL": "Pull",
"FILTER_PLACEHOLDER": "Filtro robot accounts", "FILTER_PLACEHOLDER": "Filtro robot accounts",
"ROBOT_NAME": "Não Pode conter caracteres especiais(~#$%) e comprimento máximo deveria ser 255 caracteres.", "ROBOT_NAME": "Não Pode conter caracteres especiais(~#$%) e comprimento máximo deveria ser 255 caracteres.",
"ACCOUNT_EXISTING": "Robô conta já existe.", "ACCOUNT_EXISTING": "Robô conta já existe.",
@ -325,6 +326,7 @@
"GROUP": "Grupo", "GROUP": "Grupo",
"GROUPS": "Grupos", "GROUPS": "Grupos",
"IMPORT_LDAP_GROUP": "Importar grupo do LDAP", "IMPORT_LDAP_GROUP": "Importar grupo do LDAP",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "Novo Grupo", "ADD": "Novo Grupo",
"EDIT": "Editar", "EDIT": "Editar",
"DELETE": "Remover", "DELETE": "Remover",
@ -337,8 +339,17 @@
"ADD_GROUP_SUCCESS": "Grupo adicionado com sucesso", "ADD_GROUP_SUCCESS": "Grupo adicionado com sucesso",
"EDIT_GROUP_SUCCESS": "Grupo editado com sucesso", "EDIT_GROUP_SUCCESS": "Grupo editado com sucesso",
"LDAP_TYPE": "LDAP", "LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "de", "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": { "AUDIT_LOG": {
"USERNAME": "Nome do usuário", "USERNAME": "Nome do usuário",

View File

@ -58,6 +58,7 @@
"TOOLTIP": { "TOOLTIP": {
"NAME_FILTER": "过滤资源的名字。不填或者“”匹配所有资源“library/”只匹配“library”下的资源。更多的匹配模式请参考用户手册。", "NAME_FILTER": "过滤资源的名字。不填或者“”匹配所有资源“library/”只匹配“library”下的资源。更多的匹配模式请参考用户手册。",
"TAG_FILTER": "过滤资源的tag/version。不填或者“”匹配所有“1.0*”只匹配以“1.0”开头的tag/version。", "TAG_FILTER": "过滤资源的tag/version。不填或者“”匹配所有“1.0*”只匹配以“1.0”开头的tag/version。",
"LABEL_FILTER": "根据标签筛选资源。",
"RESOURCE_FILTER": "过滤资源的类型。", "RESOURCE_FILTER": "过滤资源的类型。",
"PUSH_BASED": "把资源由本地Harbor推送到远端仓库。", "PUSH_BASED": "把资源由本地Harbor推送到远端仓库。",
"PULL_BASED": "把资源由远端仓库拉取到本地Harbor。", "PULL_BASED": "把资源由远端仓库拉取到本地Harbor。",
@ -86,8 +87,8 @@
"NONEMPTY": "不能为空", "NONEMPTY": "不能为空",
"ENDPOINT_FORMAT": "Endpoint必须以http://或https://开头。", "ENDPOINT_FORMAT": "Endpoint必须以http://或https://开头。",
"OIDC_ENDPOIT_FORMAT": "Endpoint必须以https://开头。", "OIDC_ENDPOIT_FORMAT": "Endpoint必须以https://开头。",
"OIDC_NAME": "OIDC提供商的名称.", "OIDC_NAME": "OIDC提供商的名称",
"OIDC_ENDPOINT": "OIDC服务器的地址.", "OIDC_ENDPOINT": "OIDC服务器的地址",
"OIDC_SCOPE": "在身份验证期间发送到OIDC服务器的scope。它必须包含“openid”和“offline_access”。如果您使用Google请从此字段中删除“脱机访问”。", "OIDC_SCOPE": "在身份验证期间发送到OIDC服务器的scope。它必须包含“openid”和“offline_access”。如果您使用Google请从此字段中删除“脱机访问”。",
"OIDC_VERIFYCERT": "如果您的OIDC服务器是通过自签名证书托管的请取消选中此框。" "OIDC_VERIFYCERT": "如果您的OIDC服务器是通过自签名证书托管的请取消选中此框。"
}, },
@ -298,7 +299,7 @@
"NEW_ROBOT_ACCOUNT": "添加机器人账户", "NEW_ROBOT_ACCOUNT": "添加机器人账户",
"ENABLED_STATE": "启用状态", "ENABLED_STATE": "启用状态",
"EXPIRATION": "过期时间", "EXPIRATION": "过期时间",
"NUMBER_REQUIRED":"此项为必填项且为不为0的整数.", "NUMBER_REQUIRED":"此项为必填项且为不为0的整数",
"TOKEN_EXPIRATION":"机器人账户令牌过期时间(天)", "TOKEN_EXPIRATION":"机器人账户令牌过期时间(天)",
"DESCRIPTION": "描述", "DESCRIPTION": "描述",
"ACTION": "操作", "ACTION": "操作",
@ -309,15 +310,15 @@
"ENABLE_ACCOUNT": "启用账户", "ENABLE_ACCOUNT": "启用账户",
"DELETE": "删除", "DELETE": "删除",
"CREAT_ROBOT_ACCOUNT": "创建机器人账户", "CREAT_ROBOT_ACCOUNT": "创建机器人账户",
"PULL_PERMISSION": "Pull 镜像", "PERMISSIONS_IMAGE": "镜像",
"PULL_PUSH_PERMISSION": "Push和Pull 镜像", "PERMISSIONS_HELMCHART": "Helm Chart",
"PUSH_CHART_PERMISSION": "推送Chart", "PUSH": "推送",
"PULL_CHART_PERMISSION": "拉取Chart", "PULL": "拉取",
"FILTER_PLACEHOLDER": "过滤机器人账户", "FILTER_PLACEHOLDER": "过滤机器人账户",
"ROBOT_NAME": "不能包含特殊字符(~#$%)且长度不能超过255.", "ROBOT_NAME": "不能包含特殊字符(~#$%)且长度不能超过255",
"ACCOUNT_EXISTING": "机器人账户已经存在.", "ACCOUNT_EXISTING": "机器人账户已经存在",
"ALERT_TEXT": "这是唯一一次复制您的个人访问令牌的机会", "ALERT_TEXT": "这是唯一一次复制您的个人访问令牌的机会",
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功.", "CREATED_SUCCESS": "创建账户 '{{param}}' 成功",
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌", "COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
"DELETION_TITLE": "删除账户确认", "DELETION_TITLE": "删除账户确认",
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?" "DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?"
@ -326,6 +327,7 @@
"GROUP": "组", "GROUP": "组",
"GROUPS": "组", "GROUPS": "组",
"IMPORT_LDAP_GROUP": "导入LDAP组", "IMPORT_LDAP_GROUP": "导入LDAP组",
"IMPORT_HTTP_GROUP": "新建HTTP组",
"ADD": "新增", "ADD": "新增",
"EDIT": "编辑", "EDIT": "编辑",
"DELETE": "删除", "DELETE": "删除",
@ -338,8 +340,17 @@
"ADD_GROUP_SUCCESS": "添加组成功", "ADD_GROUP_SUCCESS": "添加组成功",
"EDIT_GROUP_SUCCESS": "修改组成功", "EDIT_GROUP_SUCCESS": "修改组成功",
"LDAP_TYPE": "LDAP", "LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "共计", "OF": "共计",
"ITEMS": "条记录" "ITEMS": "条记录",
"NEW_MEMBER": "新建组成员",
"NEW_USER_INFO": "添加一个组作为具有指定角色的此项目的成员",
"ROLE": "权限",
"SYS_ADMIN": "系统管理员",
"PROJECT_ADMIN": "项目管理员",
"PROJECT_MASTER": "维护人员",
"DEVELOPER": "开发者",
"GUEST": "访客"
}, },
"AUDIT_LOG": { "AUDIT_LOG": {
"USERNAME": "用户名", "USERNAME": "用户名",
@ -727,7 +738,7 @@
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间分钟默认为30分钟。", "TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间分钟默认为30分钟。",
"ROBOT_TOKEN_EXPIRATION": "机器人账户的令牌的过期时间默认为30天,显示的结果为分钟转化的天数并向下取整。", "ROBOT_TOKEN_EXPIRATION": "机器人账户的令牌的过期时间默认为30天,显示的结果为分钟转化的天数并向下取整。",
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。", "PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书.", "ROOT_CERT_DOWNLOAD": "下载镜像库根证书",
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。", "SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。",
"VERIFY_CERT": "检查来自LDAP服务端的证书", "VERIFY_CERT": "检查来自LDAP服务端的证书",
"READONLY_TOOLTIP": "选中,表示正在维护状态,不可删除仓库及标签,也不可以推送镜像。", "READONLY_TOOLTIP": "选中,表示正在维护状态,不可删除仓库及标签,也不可以推送镜像。",
@ -859,8 +870,8 @@
}, },
"CHART": { "CHART": {
"SCANNING_TIME": "扫描完成时间:", "SCANNING_TIME": "扫描完成时间:",
"TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}.", "TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}",
"TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}.", "TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}",
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞包" "TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞包"
}, },
"SEVERITY": { "SEVERITY": {

View File

@ -87,3 +87,7 @@ body {
.color-red { .color-red {
color: red; color: red;
} }
.datagrid-table,.datagrid-header{
position: inherit !important;
}

View File

@ -17,10 +17,18 @@ package adapter
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"github.com/docker/distribution"
"github.com/goharbor/harbor/src/replication/filter"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
) )
// const definition
const (
UserAgentReplication = "harbor-replication-service"
)
var registry = map[model.RegistryType]Factory{} var registry = map[model.RegistryType]Factory{}
// Factory creates a specific Adapter according to the params // Factory creates a specific Adapter according to the params
@ -37,6 +45,81 @@ type Adapter interface {
HealthCheck() (model.HealthStatus, error) 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 // RegisterFactory registers one adapter factory to the registry
func RegisterFactory(t model.RegistryType, factory Factory) error { func RegisterFactory(t model.RegistryType, factory Factory) error {
if len(t) == 0 { if len(t) == 0 {

View 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