mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-20 15:48:26 +01:00
commit
1e2660f060
4
Makefile
4
Makefile
@ -292,7 +292,7 @@ compile_notary_migrate_patch:
|
|||||||
compile: check_environment versions_prepare compile_core compile_jobservice compile_registryctl compile_notary_migrate_patch
|
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 ; \
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
10
make/migrations/postgresql/0010_1.9.0_schema.up.sql
Normal file
10
make/migrations/postgresql/0010_1.9.0_schema.up.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* add table for CVE whitelist */
|
||||||
|
CREATE TABLE cve_whitelist (
|
||||||
|
id SERIAL PRIMARY KEY NOT NULL,
|
||||||
|
project_id int,
|
||||||
|
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||||
|
update_time timestamp default CURRENT_TIMESTAMP,
|
||||||
|
expires_at bigint,
|
||||||
|
items text NOT NULL,
|
||||||
|
UNIQUE (project_id)
|
||||||
|
);
|
@ -8,5 +8,6 @@ RUN mkdir /harbor/ \
|
|||||||
COPY ./make/photon/jobservice/start.sh ./make/photon/jobservice/harbor_jobservice /harbor/
|
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"]
|
||||||
|
74
src/common/dao/cve_whitelist.go
Normal file
74
src/common/dao/cve_whitelist.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateCVEWhitelist Updates the vulnerability white list to DB
|
||||||
|
func UpdateCVEWhitelist(l models.CVEWhitelist) (int64, error) {
|
||||||
|
o := GetOrmer()
|
||||||
|
itemsBytes, _ := json.Marshal(l.Items)
|
||||||
|
l.ItemsText = string(itemsBytes)
|
||||||
|
id, err := o.InsertOrUpdate(&l, "project_id")
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSysCVEWhitelist Gets the system level vulnerability white list from DB
|
||||||
|
func GetSysCVEWhitelist() (*models.CVEWhitelist, error) {
|
||||||
|
return GetCVEWhitelist(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSysCVEWhitelist updates the system level CVE whitelist
|
||||||
|
/*
|
||||||
|
func UpdateSysCVEWhitelist(l models.CVEWhitelist) error {
|
||||||
|
if l.ProjectID != 0 {
|
||||||
|
return fmt.Errorf("system level CVE whitelist cannot set project ID")
|
||||||
|
}
|
||||||
|
l.ProjectID = -1
|
||||||
|
_, err := UpdateCVEWhitelist(l)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// GetCVEWhitelist Gets the CVE whitelist of the project based on the project ID in parameter
|
||||||
|
func GetCVEWhitelist(pid int64) (*models.CVEWhitelist, error) {
|
||||||
|
o := GetOrmer()
|
||||||
|
qs := o.QueryTable(&models.CVEWhitelist{})
|
||||||
|
qs = qs.Filter("ProjectID", pid)
|
||||||
|
r := []*models.CVEWhitelist{}
|
||||||
|
_, err := qs.All(&r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get CVE whitelist for project %d, error: %v", pid, err)
|
||||||
|
}
|
||||||
|
if len(r) == 0 {
|
||||||
|
log.Infof("No CVE whitelist found for project %d, returning empty list.", pid)
|
||||||
|
return &models.CVEWhitelist{ProjectID: pid, Items: []models.CVEWhitelistItem{}}, nil
|
||||||
|
} else if len(r) > 1 {
|
||||||
|
log.Infof("Multiple CVE whitelists found for project %d, length: %d, returning first element.", pid, len(r))
|
||||||
|
}
|
||||||
|
items := []models.CVEWhitelistItem{}
|
||||||
|
err = json.Unmarshal([]byte(r[0].ItemsText), &items)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to decode item list, err: %v, text: %s", err, r[0].ItemsText)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r[0].Items = items
|
||||||
|
return r[0], nil
|
||||||
|
}
|
72
src/common/dao/cve_whitelist_test.go
Normal file
72
src/common/dao/cve_whitelist_test.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateAndGetCVEWhitelist(t *testing.T) {
|
||||||
|
require.Nil(t, ClearTable("cve_whitelist"))
|
||||||
|
l, err := GetSysCVEWhitelist()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, models.CVEWhitelist{ProjectID: 0, Items: []models.CVEWhitelistItem{}}, *l)
|
||||||
|
l2, err := GetCVEWhitelist(5)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, models.CVEWhitelist{ProjectID: 5, Items: []models.CVEWhitelistItem{}}, *l2)
|
||||||
|
|
||||||
|
longList := []models.CVEWhitelistItem{}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
longList = append(longList, models.CVEWhitelistItem{CVEID: "CVE-1999-0067"})
|
||||||
|
}
|
||||||
|
|
||||||
|
e := int64(1573254000)
|
||||||
|
in1 := models.CVEWhitelist{ProjectID: 3, Items: longList, ExpiresAt: &e}
|
||||||
|
_, err = UpdateCVEWhitelist(in1)
|
||||||
|
require.Nil(t, err)
|
||||||
|
// assert.Equal(t, int64(1), n)
|
||||||
|
out1, err := GetCVEWhitelist(3)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(3), out1.ProjectID)
|
||||||
|
assert.Equal(t, longList, out1.Items)
|
||||||
|
assert.Equal(t, e, *out1.ExpiresAt)
|
||||||
|
|
||||||
|
in2 := models.CVEWhitelist{ProjectID: 3, Items: []models.CVEWhitelistItem{}}
|
||||||
|
_, err = UpdateCVEWhitelist(in2)
|
||||||
|
require.Nil(t, err)
|
||||||
|
// assert.Equal(t, int64(1), n2)
|
||||||
|
out2, err := GetCVEWhitelist(3)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(3), out2.ProjectID)
|
||||||
|
assert.Equal(t, []models.CVEWhitelistItem{}, out2.Items)
|
||||||
|
|
||||||
|
sysCVEs := []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2019-10164"},
|
||||||
|
{CVEID: "CVE-2017-12345"},
|
||||||
|
}
|
||||||
|
in3 := models.CVEWhitelist{Items: sysCVEs}
|
||||||
|
_, err = UpdateCVEWhitelist(in3)
|
||||||
|
require.Nil(t, err)
|
||||||
|
// assert.Equal(t, int64(1), n3)
|
||||||
|
sysList, err := GetSysCVEWhitelist()
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(0), sysList.ProjectID)
|
||||||
|
assert.Equal(t, sysCVEs, sysList.Items)
|
||||||
|
|
||||||
|
// require.Nil(t, ClearTable("cve_whitelist"))
|
||||||
|
}
|
@ -15,10 +15,8 @@
|
|||||||
package group
|
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, ",")
|
|
||||||
}
|
|
||||||
|
@ -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,8 +292,7 @@ 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{
|
||||||
Name: "grouptestu09",
|
Name: "grouptestu09",
|
||||||
@ -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,8 +359,7 @@ 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 {
|
||||||
name string
|
name string
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
11
src/common/dao/utils.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JoinNumberConditions - To join number condition into string,used in sql query
|
||||||
|
func JoinNumberConditions(ids []int) string {
|
||||||
|
return strings.Trim(strings.Replace(fmt.Sprint(ids), " ", ",", -1), "[]")
|
||||||
|
}
|
24
src/common/dao/utils_test.go
Normal file
24
src/common/dao/utils_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestJoinNumberConditions(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ids []int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "normal test", args: args{[]int{1, 2, 3}}, want: "1,2,3"},
|
||||||
|
{name: "dummy test", args: args{[]int{}}, want: ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := JoinNumberConditions(tt.args.ids); got != tt.want {
|
||||||
|
t.Errorf("JoinNumberConditions() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -36,5 +36,6 @@ func init() {
|
|||||||
new(AdminJob),
|
new(AdminJob),
|
||||||
new(JobLog),
|
new(JobLog),
|
||||||
new(Robot),
|
new(Robot),
|
||||||
new(OIDCUser))
|
new(OIDCUser),
|
||||||
|
new(CVEWhitelist))
|
||||||
}
|
}
|
||||||
|
55
src/common/models/cve_whitelist.go
Normal file
55
src/common/models/cve_whitelist.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// CVEWhitelist defines the data model for a CVE whitelist
|
||||||
|
type CVEWhitelist struct {
|
||||||
|
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||||
|
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
|
||||||
|
ExpiresAt *int64 `orm:"column(expires_at)" json:"expires_at,omitempty"`
|
||||||
|
Items []CVEWhitelistItem `orm:"-" json:"items"`
|
||||||
|
ItemsText string `orm:"column(items)" json:"-"`
|
||||||
|
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||||
|
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CVEWhitelistItem defines one item in the CVE whitelist
|
||||||
|
type CVEWhitelistItem struct {
|
||||||
|
CVEID string `json:"cve_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName ...
|
||||||
|
func (c *CVEWhitelist) TableName() string {
|
||||||
|
return "cve_whitelist"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CVESet returns the set of CVE id of the items in the whitelist to help filter the vulnerability list
|
||||||
|
func (c *CVEWhitelist) CVESet() map[string]struct{} {
|
||||||
|
r := map[string]struct{}{}
|
||||||
|
for _, it := range c.Items {
|
||||||
|
r[it.CVEID] = struct{}{}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExpired returns whether the whitelist is expired
|
||||||
|
func (c *CVEWhitelist) IsExpired() bool {
|
||||||
|
if c.ExpiresAt == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return time.Now().Unix() >= *c.ExpiresAt
|
||||||
|
}
|
72
src/common/models/cve_whitelist_test.go
Normal file
72
src/common/models/cve_whitelist_test.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCVEWhitelist_All(t *testing.T) {
|
||||||
|
future := int64(4411494000)
|
||||||
|
now := time.Now().Unix()
|
||||||
|
cases := []struct {
|
||||||
|
input CVEWhitelist
|
||||||
|
cveset map[string]struct{}
|
||||||
|
expired bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: CVEWhitelist{
|
||||||
|
ID: 1,
|
||||||
|
ProjectID: 0,
|
||||||
|
Items: []CVEWhitelistItem{},
|
||||||
|
},
|
||||||
|
cveset: map[string]struct{}{},
|
||||||
|
expired: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: CVEWhitelist{
|
||||||
|
ID: 1,
|
||||||
|
ProjectID: 0,
|
||||||
|
Items: []CVEWhitelistItem{},
|
||||||
|
ExpiresAt: &now,
|
||||||
|
},
|
||||||
|
cveset: map[string]struct{}{},
|
||||||
|
expired: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: CVEWhitelist{
|
||||||
|
ID: 2,
|
||||||
|
ProjectID: 3,
|
||||||
|
Items: []CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-1999-0067"},
|
||||||
|
{CVEID: "CVE-2016-7654321"},
|
||||||
|
},
|
||||||
|
ExpiresAt: &future,
|
||||||
|
},
|
||||||
|
cveset: map[string]struct{}{
|
||||||
|
"CVE-1999-0067": {},
|
||||||
|
"CVE-2016-7654321": {},
|
||||||
|
},
|
||||||
|
expired: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
assert.Equal(t, c.expired, c.input.IsExpired())
|
||||||
|
assert.True(t, reflect.DeepEqual(c.cveset, c.input.CVESet()))
|
||||||
|
}
|
||||||
|
}
|
@ -20,16 +20,17 @@ import (
|
|||||||
|
|
||||||
// keys of project metadata and severity values
|
// keys of project metadata and severity values
|
||||||
const (
|
const (
|
||||||
ProMetaPublic = "public"
|
ProMetaPublic = "public"
|
||||||
ProMetaEnableContentTrust = "enable_content_trust"
|
ProMetaEnableContentTrust = "enable_content_trust"
|
||||||
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"
|
||||||
SeverityNone = "negligible"
|
ProMetaReuseSysCVEWhitelist = "reuse_sys_cve_whitelist"
|
||||||
SeverityLow = "low"
|
SeverityNone = "negligible"
|
||||||
SeverityMedium = "medium"
|
SeverityLow = "low"
|
||||||
SeverityHigh = "high"
|
SeverityMedium = "medium"
|
||||||
SeverityCritical = "critical"
|
SeverityHigh = "high"
|
||||||
|
SeverityCritical = "critical"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProjectMetadata holds the metadata of a project.
|
// ProjectMetadata holds the metadata of a project.
|
||||||
|
@ -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)
|
||||||
@ -128,9 +138,9 @@ type ProjectQueryParam struct {
|
|||||||
|
|
||||||
// MemberQuery filter by member's username and role
|
// MemberQuery filter by member's username and role
|
||||||
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 ...
|
||||||
@ -154,9 +164,10 @@ type BaseProjectCollection struct {
|
|||||||
|
|
||||||
// ProjectRequest holds informations that need for creating project API
|
// ProjectRequest holds informations that need for creating project API
|
||||||
type ProjectRequest struct {
|
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 ...
|
||||||
|
@ -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
26
src/common/models/sev.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Severity represents the severity of a image/component in terms of vulnerability.
|
||||||
|
type Severity int64
|
||||||
|
|
||||||
|
// Sevxxx is the list of severity of image after scanning.
|
||||||
|
const (
|
||||||
|
_ Severity = iota
|
||||||
|
SevNone
|
||||||
|
SevUnknown
|
||||||
|
SevLow
|
||||||
|
SevMedium
|
||||||
|
SevHigh
|
||||||
|
)
|
||||||
|
|
||||||
|
// String is the output function for severity variable
|
||||||
|
func (sev Severity) String() string {
|
||||||
|
name := []string{"negligible", "unknown", "low", "medium", "high"}
|
||||||
|
i := int64(sev)
|
||||||
|
switch {
|
||||||
|
case i >= 1 && i <= int64(SevHigh):
|
||||||
|
return name[i-1]
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
@ -35,13 +35,13 @@ type User struct {
|
|||||||
// to it.
|
// to it.
|
||||||
Role int `orm:"-" json:"role_id"`
|
Role int `orm:"-" json:"role_id"`
|
||||||
// RoleList []Role `json:"role_list"`
|
// RoleList []Role `json:"role_list"`
|
||||||
HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"`
|
HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"`
|
||||||
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
|
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserQuery ...
|
// UserQuery ...
|
||||||
|
@ -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},
|
||||||
|
@ -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},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
@ -157,8 +155,8 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
|
|||||||
result, err := s.pm.List(
|
result, err := s.pm.List(
|
||||||
&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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
114
src/common/utils/redis/helper.go
Normal file
114
src/common/utils/redis/helper.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnLock ...
|
||||||
|
ErrUnLock = errors.New("error to release the redis lock")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
unlockScript = `
|
||||||
|
if redis.call("get",KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call("del",KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`
|
||||||
|
defaultDelay = 5 * time.Second
|
||||||
|
defaultMaxRetry = 5
|
||||||
|
defaultExpiry = 600 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mutex ...
|
||||||
|
type Mutex struct {
|
||||||
|
Conn redis.Conn
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
// New ...
|
||||||
|
func New(conn redis.Conn, key, value string) *Mutex {
|
||||||
|
o := *DefaultOptions()
|
||||||
|
if value == "" {
|
||||||
|
value = utils.GenerateRandomString()
|
||||||
|
}
|
||||||
|
return &Mutex{conn, key, value, o}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require retry to require the lock
|
||||||
|
func (rm *Mutex) Require() (bool, error) {
|
||||||
|
var isRequired bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for i := 0; i < rm.opts.maxRetry; i++ {
|
||||||
|
isRequired, err = rm.require()
|
||||||
|
if isRequired {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil || !isRequired {
|
||||||
|
time.Sleep(rm.opts.retryDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRequired, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// require get the redis lock, for details, just refer to https://redis.io/topics/distlock
|
||||||
|
func (rm *Mutex) require() (bool, error) {
|
||||||
|
reply, err := redis.String(rm.Conn.Do("SET", rm.key, rm.value, "NX", "PX", int(rm.opts.expiry/time.Millisecond)))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return reply == "OK", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free releases the lock, for details, just refer to https://redis.io/topics/distlock
|
||||||
|
func (rm *Mutex) Free() (bool, error) {
|
||||||
|
script := redis.NewScript(1, unlockScript)
|
||||||
|
resp, err := redis.Int(script.Do(rm.Conn, rm.key, rm.value))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if resp == 0 {
|
||||||
|
return false, ErrUnLock
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options ...
|
||||||
|
type Options struct {
|
||||||
|
retryDelay time.Duration
|
||||||
|
expiry time.Duration
|
||||||
|
maxRetry int
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions ...
|
||||||
|
func DefaultOptions() *Options {
|
||||||
|
opt := &Options{
|
||||||
|
retryDelay: defaultDelay,
|
||||||
|
expiry: defaultExpiry,
|
||||||
|
maxRetry: defaultMaxRetry,
|
||||||
|
}
|
||||||
|
return opt
|
||||||
|
}
|
62
src/common/utils/redis/helper_test.go
Normal file
62
src/common/utils/redis/helper_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testingRedisHost = "REDIS_HOST"
|
||||||
|
|
||||||
|
func TestRedisLock(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
rm := New(con, "test-redis-lock", "test-value")
|
||||||
|
|
||||||
|
successLock, err := rm.Require()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, successLock)
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
_, err = rm.Require()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
successUnLock, err := rm.Free()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, successUnLock)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRedisHost() string {
|
||||||
|
redisHost := os.Getenv(testingRedisHost)
|
||||||
|
if redisHost == "" {
|
||||||
|
redisHost = "127.0.0.1" // for local test
|
||||||
|
}
|
||||||
|
|
||||||
|
return redisHost
|
||||||
|
}
|
@ -22,8 +22,6 @@ import (
|
|||||||
"net/url"
|
"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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -460,7 +461,8 @@ 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)
|
||||||
|
@ -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{
|
||||||
|
@ -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"`
|
||||||
|
@ -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()
|
||||||
|
81
src/core/api/sys_cve_whitelist.go
Normal file
81
src/core/api/sys_cve_whitelist.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SysCVEWhitelistAPI Handles the requests to manage system level CVE whitelist
|
||||||
|
type SysCVEWhitelistAPI struct {
|
||||||
|
BaseController
|
||||||
|
manager whitelist.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare validates the request initially
|
||||||
|
func (sca *SysCVEWhitelistAPI) Prepare() {
|
||||||
|
sca.BaseController.Prepare()
|
||||||
|
if !sca.SecurityCtx.IsAuthenticated() {
|
||||||
|
sca.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !sca.SecurityCtx.IsSysAdmin() && sca.Ctx.Request.Method != http.MethodGet {
|
||||||
|
msg := fmt.Sprintf("only system admin has permission issue %s request to this API", sca.Ctx.Request.Method)
|
||||||
|
log.Errorf(msg)
|
||||||
|
sca.SendForbiddenError(errors.New(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sca.manager = whitelist.NewDefaultManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get handles the GET request to retrieve the system level CVE whitelist
|
||||||
|
func (sca *SysCVEWhitelistAPI) Get() {
|
||||||
|
l, err := sca.manager.GetSys()
|
||||||
|
if err != nil {
|
||||||
|
sca.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sca.WriteJSONData(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put handles the PUT request to update the system level CVE whitelist
|
||||||
|
func (sca *SysCVEWhitelistAPI) Put() {
|
||||||
|
var l models.CVEWhitelist
|
||||||
|
if err := sca.DecodeJSONReq(&l); err != nil {
|
||||||
|
log.Errorf("Failed to decode JSON array from request")
|
||||||
|
sca.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if l.ProjectID != 0 {
|
||||||
|
msg := fmt.Sprintf("Non-zero project ID for system CVE whitelist: %d.", l.ProjectID)
|
||||||
|
log.Error(msg)
|
||||||
|
sca.SendBadRequestError(errors.New(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sca.manager.SetSys(l); err != nil {
|
||||||
|
if whitelist.IsInvalidErr(err) {
|
||||||
|
log.Errorf("Invalid CVE whitelist: %v", err)
|
||||||
|
sca.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sca.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
126
src/core/api/sys_cve_whitelist_test.go
Normal file
126
src/core/api/sys_cve_whitelist_test.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSysCVEWhitelistAPIGet(t *testing.T) {
|
||||||
|
url := "/api/system/CVEWhitelist"
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSysCVEWhitelistAPIPut(t *testing.T) {
|
||||||
|
url := "/api/system/CVEWhitelist"
|
||||||
|
s := int64(1573254000)
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 400
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
bodyJSON: []string{"CVE-1234-1234"},
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
// 400
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
bodyJSON: models.CVEWhitelist{
|
||||||
|
ExpiresAt: &s,
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2019-12310"},
|
||||||
|
},
|
||||||
|
ProjectID: 2,
|
||||||
|
},
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
// 400
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
bodyJSON: models.CVEWhitelist{
|
||||||
|
ExpiresAt: &s,
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2019-12310"},
|
||||||
|
{CVEID: "CVE-2019-12310"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
bodyJSON: models.CVEWhitelist{
|
||||||
|
ExpiresAt: &s,
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2019-12310"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
@ -24,7 +24,6 @@ import (
|
|||||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
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
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,9 +93,12 @@ 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 {
|
||||||
if err = d.metaMgr.Add(id, project.Metadata); err != nil {
|
d.whitelistMgr.CreateEmpty(project.ProjectID)
|
||||||
log.Errorf("failed to add metadata for project %s: %v", project.Name, err)
|
if len(project.Metadata) > 0 {
|
||||||
|
if err = d.metaMgr.Add(id, project.Metadata); err != nil {
|
||||||
|
log.Errorf("failed to add metadata for project %s: %v", project.Name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
@ -110,37 +121,40 @@ 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
|
||||||
|
}
|
||||||
|
if pro == nil {
|
||||||
|
return fmt.Errorf("project %v not found", projectIDOrName)
|
||||||
|
}
|
||||||
|
// TODO transaction?
|
||||||
|
if d.metaMgrEnabled {
|
||||||
|
if err := d.whitelistMgr.Set(pro.ProjectID, project.CVEWhitelist); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if pro == nil {
|
if len(project.Metadata) > 0 {
|
||||||
return fmt.Errorf("project %v not found", projectIDOrName)
|
metaNeedUpdated := map[string]string{}
|
||||||
}
|
metaNeedCreated := map[string]string{}
|
||||||
|
if pro.Metadata == nil {
|
||||||
// TODO transaction?
|
pro.Metadata = map[string]string{}
|
||||||
metaNeedUpdated := map[string]string{}
|
}
|
||||||
metaNeedCreated := map[string]string{}
|
for key, value := range project.Metadata {
|
||||||
if pro.Metadata == nil {
|
_, exist := pro.Metadata[key]
|
||||||
pro.Metadata = map[string]string{}
|
if exist {
|
||||||
}
|
metaNeedUpdated[key] = value
|
||||||
for key, value := range project.Metadata {
|
} else {
|
||||||
_, exist := pro.Metadata[key]
|
metaNeedCreated[key] = value
|
||||||
if exist {
|
}
|
||||||
metaNeedUpdated[key] = value
|
}
|
||||||
} else {
|
if err = d.metaMgr.Add(pro.ProjectID, metaNeedCreated); err != nil {
|
||||||
metaNeedCreated[key] = value
|
return err
|
||||||
|
}
|
||||||
|
if err = d.metaMgr.Update(pro.ProjectID, metaNeedUpdated); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = d.metaMgr.Add(pro.ProjectID, metaNeedCreated); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = d.metaMgr.Update(pro.ProjectID, metaNeedUpdated); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.pmsDriver.Update(projectIDOrName, project)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,9 +166,10 @@ func TestPMSPolicyChecker(t *testing.T) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
OwnerID: 1,
|
OwnerID: 1,
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
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) {
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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{})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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=
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
count, err := redis.Int(conn.Do("ZCARD", key))
|
tk := time.NewTicker(500 * time.Millisecond)
|
||||||
require.Nil(suite.T(), err, "count scheduled: nil error expected but got %s", err)
|
defer tk.Stop()
|
||||||
assert.Condition(suite.T(), func() bool {
|
|
||||||
return count > 0
|
for {
|
||||||
}, "count of scheduled jobs should be greater than 0 but got %d", count)
|
select {
|
||||||
|
case <-tk.C:
|
||||||
|
count, err := redis.Int(conn.Do("ZCARD", key))
|
||||||
|
require.Nil(suite.T(), err, "count scheduled: nil error expected but got %s", err)
|
||||||
|
if assert.Condition(suite.T(), func() (success bool) {
|
||||||
|
return count > 0
|
||||||
|
}, "at least one job should be scheduled for the periodic job policy") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-time.After(15 * time.Second):
|
||||||
|
require.NoError(suite.T(), errors.New("timeout (15s): expect at 1 scheduled job but still get nothing"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := suite.enqueuer.start()
|
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
136
src/pkg/scan/vuln.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package scan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/clair"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VulnerabilityItem represents a vulnerability reported by scanner
|
||||||
|
type VulnerabilityItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Severity models.Severity `json:"severity"`
|
||||||
|
Pkg string `json:"package"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Fixed string `json:"fixedVersion,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VulnerabilityList is a list of vulnerabilities, which should be scanner-agnostic
|
||||||
|
type VulnerabilityList []VulnerabilityItem
|
||||||
|
|
||||||
|
// ApplyWhitelist filters out the CVE defined in the whitelist in the parm.
|
||||||
|
// It returns the items that are filtered for the caller to track or log.
|
||||||
|
func (vl *VulnerabilityList) ApplyWhitelist(whitelist models.CVEWhitelist) VulnerabilityList {
|
||||||
|
filtered := VulnerabilityList{}
|
||||||
|
if whitelist.IsExpired() {
|
||||||
|
log.Info("The input whitelist is expired, skip filtering")
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
s := whitelist.CVESet()
|
||||||
|
r := (*vl)[:0]
|
||||||
|
for _, v := range *vl {
|
||||||
|
if _, ok := s[v.ID]; ok {
|
||||||
|
log.Debugf("Filtered Vulnerability in whitelist, CVE ID: %s, severity: %s", v.ID, v.Severity)
|
||||||
|
filtered = append(filtered, v)
|
||||||
|
} else {
|
||||||
|
r = append(r, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val := reflect.ValueOf(vl)
|
||||||
|
val.Elem().SetLen(len(r))
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Severity returns the highest severity of the vulnerabilities in the list
|
||||||
|
func (vl *VulnerabilityList) Severity() models.Severity {
|
||||||
|
s := models.SevNone
|
||||||
|
for _, v := range *vl {
|
||||||
|
if v.Severity > s {
|
||||||
|
s = v.Severity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasCVE returns whether the vulnerability list has the vulnerability with CVE ID in the parm
|
||||||
|
func (vl *VulnerabilityList) HasCVE(id string) bool {
|
||||||
|
for _, v := range *vl {
|
||||||
|
if v.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// VulnListFromClairResult transforms the returned value of Clair API to a VulnerabilityList
|
||||||
|
func VulnListFromClairResult(layerWithVuln *models.ClairLayerEnvelope) VulnerabilityList {
|
||||||
|
res := VulnerabilityList{}
|
||||||
|
if layerWithVuln == nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
l := layerWithVuln.Layer
|
||||||
|
if l == nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
features := l.Features
|
||||||
|
if features == nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
for _, f := range features {
|
||||||
|
vulnerabilities := f.Vulnerabilities
|
||||||
|
if vulnerabilities == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, v := range vulnerabilities {
|
||||||
|
vItem := VulnerabilityItem{
|
||||||
|
ID: v.Name,
|
||||||
|
Pkg: f.Name,
|
||||||
|
Version: f.Version,
|
||||||
|
Severity: clair.ParseClairSev(v.Severity),
|
||||||
|
Fixed: v.FixedBy,
|
||||||
|
Link: v.Link,
|
||||||
|
Description: v.Description,
|
||||||
|
}
|
||||||
|
res = append(res, vItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// VulnListByDigest returns the VulnerabilityList based on the scan result of artifact with the digest in the parm
|
||||||
|
func VulnListByDigest(digest string) (VulnerabilityList, error) {
|
||||||
|
var res VulnerabilityList
|
||||||
|
overview, err := dao.GetImgScanOverview(digest)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
if overview == nil || len(overview.DetailsKey) == 0 {
|
||||||
|
return res, fmt.Errorf("unable to get the scan result for digest: %s, the artifact is not scanned", digest)
|
||||||
|
}
|
||||||
|
c := clair.NewClient(config.ClairEndpoint(), nil)
|
||||||
|
clairRes, err := c.GetResult(overview.DetailsKey)
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("failed to get scan result from Clair, error: %v", err)
|
||||||
|
}
|
||||||
|
return VulnListFromClairResult(clairRes), nil
|
||||||
|
}
|
178
src/pkg/scan/vuln_test.go
Normal file
178
src/pkg/scan/vuln_test.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package scan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
past = int64(1561967574)
|
||||||
|
vulnList1 = VulnerabilityList{}
|
||||||
|
vulnList2 = VulnerabilityList{
|
||||||
|
{ID: "CVE-2018-10754",
|
||||||
|
Severity: models.SevLow,
|
||||||
|
Pkg: "ncurses",
|
||||||
|
Version: "6.0+20161126-1+deb9u2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "CVE-2018-6485",
|
||||||
|
Severity: models.SevHigh,
|
||||||
|
Pkg: "glibc",
|
||||||
|
Version: "2.24-11+deb9u4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
whiteList1 = models.CVEWhitelist{
|
||||||
|
ExpiresAt: &past,
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2018-6485"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
whiteList2 = models.CVEWhitelist{
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2018-6485"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
whiteList3 = models.CVEWhitelist{
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2018-6485"},
|
||||||
|
{CVEID: "CVE-2018-10754"},
|
||||||
|
{CVEID: "CVE-2019-12817"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
dao.PrepareTestForPostgresSQL()
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVulnerabilityList_HasCVE(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input VulnerabilityList
|
||||||
|
cve string
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: vulnList1,
|
||||||
|
cve: "CVE-2018-10754",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: vulnList2,
|
||||||
|
cve: "CVE-2018-10754",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
assert.Equal(t, c.result, c.input.HasCVE(c.cve))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVulnerabilityList_Severity(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input VulnerabilityList
|
||||||
|
expect models.Severity
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: vulnList1,
|
||||||
|
expect: models.SevNone,
|
||||||
|
}, {
|
||||||
|
input: vulnList2,
|
||||||
|
expect: models.SevHigh,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
assert.Equal(t, c.expect, c.input.Severity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVulnerabilityList_ApplyWhitelist(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
vl VulnerabilityList
|
||||||
|
wl models.CVEWhitelist
|
||||||
|
expectFiltered VulnerabilityList
|
||||||
|
expectSev models.Severity
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
vl: vulnList2,
|
||||||
|
wl: whiteList1,
|
||||||
|
expectFiltered: VulnerabilityList{},
|
||||||
|
expectSev: models.SevHigh,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vl: vulnList2,
|
||||||
|
wl: whiteList2,
|
||||||
|
expectFiltered: VulnerabilityList{
|
||||||
|
{
|
||||||
|
ID: "CVE-2018-6485",
|
||||||
|
Severity: models.SevHigh,
|
||||||
|
Pkg: "glibc",
|
||||||
|
Version: "2.24-11+deb9u4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectSev: models.SevLow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vl: vulnList1,
|
||||||
|
wl: whiteList3,
|
||||||
|
expectFiltered: VulnerabilityList{},
|
||||||
|
expectSev: models.SevNone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vl: vulnList2,
|
||||||
|
wl: whiteList3,
|
||||||
|
expectFiltered: VulnerabilityList{
|
||||||
|
{ID: "CVE-2018-10754",
|
||||||
|
Severity: models.SevLow,
|
||||||
|
Pkg: "ncurses",
|
||||||
|
Version: "6.0+20161126-1+deb9u2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "CVE-2018-6485",
|
||||||
|
Severity: models.SevHigh,
|
||||||
|
Pkg: "glibc",
|
||||||
|
Version: "2.24-11+deb9u4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectSev: models.SevNone,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
filtered := c.vl.ApplyWhitelist(c.wl)
|
||||||
|
assert.Equal(t, c.expectFiltered, filtered)
|
||||||
|
assert.Equal(t, c.vl.Severity(), c.expectSev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVulnListByDigest(t *testing.T) {
|
||||||
|
_, err := VulnListByDigest("notexist")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVulnListFromClairResult(t *testing.T) {
|
||||||
|
l := VulnListFromClairResult(nil)
|
||||||
|
assert.Equal(t, VulnerabilityList{}, l)
|
||||||
|
lv := &models.ClairLayerEnvelope{
|
||||||
|
Layer: nil,
|
||||||
|
Error: nil,
|
||||||
|
}
|
||||||
|
l2 := VulnListFromClairResult(lv)
|
||||||
|
assert.Equal(t, VulnerabilityList{}, l2)
|
||||||
|
}
|
79
src/pkg/scan/whitelist/manager.go
Normal file
79
src/pkg/scan/whitelist/manager.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package whitelist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager defines the interface of CVE whitelist manager, it support both system level and project level whitelists
|
||||||
|
type Manager interface {
|
||||||
|
// CreateEmpty creates empty whitelist for given project
|
||||||
|
CreateEmpty(projectID int64) error
|
||||||
|
// Set sets the whitelist for given project (create or update)
|
||||||
|
Set(projectID int64, list models.CVEWhitelist) error
|
||||||
|
// Get gets the whitelist for given project
|
||||||
|
Get(projectID int64) (*models.CVEWhitelist, error)
|
||||||
|
// SetSys sets system level whitelist
|
||||||
|
SetSys(list models.CVEWhitelist) error
|
||||||
|
// GetSys gets system level whitelist
|
||||||
|
GetSys() (*models.CVEWhitelist, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultManager struct{}
|
||||||
|
|
||||||
|
// CreateEmpty creates empty whitelist for given project
|
||||||
|
func (d *defaultManager) CreateEmpty(projectID int64) error {
|
||||||
|
l := models.CVEWhitelist{
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
_, err := dao.UpdateCVEWhitelist(l)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to create empty CVE whitelist for project: %d, error: %v", projectID, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the whitelist for given project (create or update)
|
||||||
|
func (d *defaultManager) Set(projectID int64, list models.CVEWhitelist) error {
|
||||||
|
list.ProjectID = projectID
|
||||||
|
if err := Validate(list); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := dao.UpdateCVEWhitelist(list)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets the whitelist for given project
|
||||||
|
func (d *defaultManager) Get(projectID int64) (*models.CVEWhitelist, error) {
|
||||||
|
return dao.GetCVEWhitelist(projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSys sets the system level whitelist
|
||||||
|
func (d *defaultManager) SetSys(list models.CVEWhitelist) error {
|
||||||
|
return d.Set(0, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSys gets the system level whitelist
|
||||||
|
func (d *defaultManager) GetSys() (*models.CVEWhitelist, error) {
|
||||||
|
return d.Get(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultManager return a new instance of defaultManager
|
||||||
|
func NewDefaultManager() Manager {
|
||||||
|
return &defaultManager{}
|
||||||
|
}
|
60
src/pkg/scan/whitelist/validator.go
Normal file
60
src/pkg/scan/whitelist/validator.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package whitelist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type invalidErr struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ie *invalidErr) Error() string {
|
||||||
|
return ie.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInvalidErr ...
|
||||||
|
func NewInvalidErr(s string) error {
|
||||||
|
return &invalidErr{
|
||||||
|
msg: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInvalidErr checks if the error is an invalidErr
|
||||||
|
func IsInvalidErr(err error) bool {
|
||||||
|
_, ok := err.(*invalidErr)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
const cveIDPattern = `^CVE-\d{4}-\d+$`
|
||||||
|
|
||||||
|
// Validate help validates the CVE whitelist, to ensure the CVE ID is valid and there's no duplication
|
||||||
|
func Validate(wl models.CVEWhitelist) error {
|
||||||
|
m := map[string]struct{}{}
|
||||||
|
re := regexp.MustCompile(cveIDPattern)
|
||||||
|
for _, it := range wl.Items {
|
||||||
|
if !re.MatchString(it.CVEID) {
|
||||||
|
return &invalidErr{fmt.Sprintf("invalid CVE ID: %s", it.CVEID)}
|
||||||
|
}
|
||||||
|
if _, ok := m[it.CVEID]; ok {
|
||||||
|
return &invalidErr{fmt.Sprintf("duplicate CVE ID in whitelist: %s", it.CVEID)}
|
||||||
|
}
|
||||||
|
m[it.CVEID] = struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
102
src/pkg/scan/whitelist/validator_test.go
Normal file
102
src/pkg/scan/whitelist/validator_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package whitelist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsInvalidErr(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
instance error
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
instance: nil,
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
instance: fmt.Errorf("whatever"),
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
instance: NewInvalidErr("This is true"),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, c := range cases {
|
||||||
|
t.Logf("Executing TestIsInvalidErr case: %d\n", n)
|
||||||
|
assert.Equal(t, c.expect, IsInvalidErr(c.instance))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
l models.CVEWhitelist
|
||||||
|
noError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
l: models.CVEWhitelist{
|
||||||
|
Items: nil,
|
||||||
|
},
|
||||||
|
noError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
l: models.CVEWhitelist{
|
||||||
|
Items: []models.CVEWhitelistItem{},
|
||||||
|
},
|
||||||
|
noError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
l: models.CVEWhitelist{
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "breakit"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
l: models.CVEWhitelist{
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2014-456132"},
|
||||||
|
{CVEID: "CVE-2014-7654321"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
l: models.CVEWhitelist{
|
||||||
|
Items: []models.CVEWhitelistItem{
|
||||||
|
{CVEID: "CVE-2014-456132"},
|
||||||
|
{CVEID: "CVE-2014-456132"},
|
||||||
|
{CVEID: "CVE-2014-7654321"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for n, c := range cases {
|
||||||
|
t.Logf("Executing TestValidate case: %d\n", n)
|
||||||
|
e := Validate(c.l)
|
||||||
|
assert.Equal(t, c.noError, e == nil)
|
||||||
|
if e != nil {
|
||||||
|
assert.True(t, IsInvalidErr(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -44,11 +44,15 @@
|
|||||||
<div class="form-group">
|
<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>
|
||||||
|
@ -15,3 +15,6 @@
|
|||||||
.inputWidth {
|
.inputWidth {
|
||||||
width: 216px;
|
width: 216px;
|
||||||
}
|
}
|
||||||
|
.display-none {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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,7 +533,11 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!findTag) {
|
if (!findTag) {
|
||||||
filtersArray.push({ type: this.supportedFilters[i].type, value: "" });
|
if (this.supportedFilters[i].type === FilterType.LABEL) {
|
||||||
|
filtersArray.push({ type: this.supportedFilters[i].type, value: [] });
|
||||||
|
} else {
|
||||||
|
filtersArray.push({ type: this.supportedFilters[i].type, value: "" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -482,4 +557,20 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return trigger_settingsControls.controls.cron.touched || trigger_settingsControls.controls.cron.dirty;
|
return trigger_settingsControls.controls.cron.touched || trigger_settingsControls.controls.cron.dirty;
|
||||||
}
|
}
|
||||||
|
stickLabel(value, index) {
|
||||||
|
value.select = !value.select;
|
||||||
|
let filters = this.ruleForm.get('filters') as FormArray;
|
||||||
|
let fromIndex = filters.controls[index] as FormGroup;
|
||||||
|
let labelValue = this.supportedFilterLabels.reduce( (cumulatedSelectedArrs, currentValue) => {
|
||||||
|
if (currentValue.select) {
|
||||||
|
if (!cumulatedSelectedArrs.length) {
|
||||||
|
return [currentValue.name];
|
||||||
|
}
|
||||||
|
return [...cumulatedSelectedArrs, currentValue.name];
|
||||||
|
}
|
||||||
|
return cumulatedSelectedArrs;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
fromIndex.setControl('value', this.fb.array(labelValue));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,7 @@ export interface ReplicationRule extends Base {
|
|||||||
|
|
||||||
export class Filter {
|
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;
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
@ -70,7 +77,7 @@ export class HarborShellComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.searchCloseSub = this.searchTrigger.searchCloseChan$.subscribe(close => {
|
this.searchCloseSub = this.searchTrigger.searchCloseChan$.subscribe(close => {
|
||||||
this.isSearchResultsOpened = false;
|
this.isSearchResultsOpened = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,11 +104,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
|
|||||||
return account != null && account.has_admin_role;
|
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;
|
||||||
|
@ -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>
|
||||||
|
@ -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,25 +32,36 @@ 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() { }
|
||||||
|
|
||||||
public get isDNInvalid(): boolean {
|
public get isDNInvalid(): boolean {
|
||||||
let dnControl = this.groupForm.controls['ldap_group_dn'];
|
let dnControl = this.groupForm.controls['ldap_group_dn'];
|
||||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||||
}
|
}
|
||||||
public get isNameInvalid(): boolean {
|
public get isNameInvalid(): boolean {
|
||||||
let dnControl = this.groupForm.controls['group_name'];
|
let dnControl = this.groupForm.controls['group_name'];
|
||||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isFormValid(): boolean {
|
public get isFormValid(): boolean {
|
||||||
@ -83,7 +96,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
|||||||
let groupCopy = Object.assign({}, this.group);
|
let groupCopy = Object.assign({}, this.group);
|
||||||
this.groupService
|
this.groupService
|
||||||
.createGroup(groupCopy).pipe(
|
.createGroup(groupCopy).pipe(
|
||||||
finalize(() => this.close()))
|
finalize(() => this.close()))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
res => {
|
res => {
|
||||||
this.msgHandler.showSuccess("GROUP.ADD_GROUP_SUCCESS");
|
this.msgHandler.showSuccess("GROUP.ADD_GROUP_SUCCESS");
|
||||||
@ -97,7 +110,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
|||||||
let groupCopy = Object.assign({}, this.group);
|
let groupCopy = Object.assign({}, this.group);
|
||||||
this.groupService
|
this.groupService
|
||||||
.editGroup(groupCopy).pipe(
|
.editGroup(groupCopy).pipe(
|
||||||
finalize(() => this.close()))
|
finalize(() => this.close()))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
res => {
|
res => {
|
||||||
this.msgHandler.showSuccess("GROUP.EDIT_GROUP_SUCCESS");
|
this.msgHandler.showSuccess("GROUP.EDIT_GROUP_SUCCESS");
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,18 +15,18 @@
|
|||||||
<clr-icon shape="plus" size="15"></clr-icon> {{'GROUP.ADD' | translate}}</button>
|
<clr-icon shape="plus" size="15"></clr-icon> {{'GROUP.ADD' | translate}}</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary" (click)="editGroup()" [disabled]="!canEditGroup">
|
<button type="button" class="btn btn-sm btn-secondary" (click)="editGroup()" [disabled]="!canEditGroup">
|
||||||
<clr-icon shape="pencil" size="15"></clr-icon> {{'GROUP.EDIT' | translate}}</button>
|
<clr-icon shape="pencil" size="15"></clr-icon> {{'GROUP.EDIT' | translate}}</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary" (click)="openDeleteConfirmationDialog()" [disabled]="!canEditGroup">
|
<button type="button" class="btn btn-sm btn-secondary" (click)="openDeleteConfirmationDialog()" [disabled]="!canDeleteGroup">
|
||||||
<clr-icon shape="times" size="15"></clr-icon> {{'GROUP.DELETE' | translate}}</button>
|
<clr-icon shape="times" size="15"></clr-icon> {{'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">
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = [];
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
<clr-modal [(clrModalOpen)]="addHttpAuthOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||||
|
<h3 class="modal-title">{{'GROUP.NEW_MEMBER' | translate}}</h3>
|
||||||
|
<inline-alert class="modal-title padding-0"></inline-alert>
|
||||||
|
<div class="modal-body">
|
||||||
|
<label>{{ 'GROUP.NEW_USER_INFO' | translate}}</label>
|
||||||
|
|
||||||
|
<form #memberForm="ngForm">
|
||||||
|
<section class="form-block">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="member_name" class="col-md-4 form-group-label-override required">{{'GROUP.GROUP' | translate}} {{'GROUP.NAME' | translate}}</label>
|
||||||
|
<label for="member_name" aria-haspopup="true" role="tooltip"
|
||||||
|
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
|
||||||
|
<input type="text" id="member_name" [(ngModel)]="member_group.group_name"
|
||||||
|
name="member_name"
|
||||||
|
size="20"
|
||||||
|
minlength="3"
|
||||||
|
#memberName="ngModel"
|
||||||
|
required autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-4 form-group-label-override">{{'GROUP.ROLE' | translate}}</label>
|
||||||
|
<div class="radio" *ngFor="let projectRoot of projectRoots">
|
||||||
|
<input type="radio" name="member_role" id="{{'check_root_project_' + projectRoot.NAME}}" [value]="projectRoot.VALUE" [(ngModel)]="role_id">
|
||||||
|
<label for="{{'check_root_project_' + projectRoot.NAME}}">{{ projectRoot.LABEL | translate}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||||
|
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
@ -0,0 +1,8 @@
|
|||||||
|
.form-group-label-override {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.padding-0 {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AddHttpAuthGroupComponent } from './add-http-auth-group.component';
|
||||||
|
|
||||||
|
describe('AddHttpAuthGroupComponent', () => {
|
||||||
|
let component: AddHttpAuthGroupComponent;
|
||||||
|
let fixture: ComponentFixture<AddHttpAuthGroupComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ AddHttpAuthGroupComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AddHttpAuthGroupComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,120 @@
|
|||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
EventEmitter,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { NgForm } from '@angular/forms';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { InlineAlertComponent } from '../../../shared/inline-alert/inline-alert.component';
|
||||||
|
import { UserService } from '../../../user/user.service';
|
||||||
|
|
||||||
|
|
||||||
|
import { errorHandler as errorHandFn, PROJECT_ROOTS, ProjectRootInterface } from "@harbor/ui";
|
||||||
|
|
||||||
|
import { MemberService } from '../member.service';
|
||||||
|
import { UserGroup } from "./../../../group/group";
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'add-http-auth-group',
|
||||||
|
templateUrl: './add-http-auth-group.component.html',
|
||||||
|
styleUrls: ['./add-http-auth-group.component.scss'],
|
||||||
|
providers: [UserService]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class AddHttpAuthGroupComponent implements OnInit {
|
||||||
|
projectRoots: ProjectRootInterface[];
|
||||||
|
member_group: UserGroup = { group_name: '', group_type: 2 };
|
||||||
|
role_id: number;
|
||||||
|
addHttpAuthOpened: boolean;
|
||||||
|
|
||||||
|
memberForm: NgForm;
|
||||||
|
|
||||||
|
staticBackdrop: boolean = true;
|
||||||
|
closable: boolean = false;
|
||||||
|
|
||||||
|
@ViewChild('memberForm')
|
||||||
|
currentForm: NgForm;
|
||||||
|
|
||||||
|
@ViewChild(InlineAlertComponent)
|
||||||
|
inlineAlert: InlineAlertComponent;
|
||||||
|
|
||||||
|
@Input() projectId: number;
|
||||||
|
@Output() added = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
checkOnGoing: boolean = false;
|
||||||
|
|
||||||
|
constructor(private memberService: MemberService,
|
||||||
|
private translateService: TranslateService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.projectRoots = PROJECT_ROOTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
createGroupAsMember() {
|
||||||
|
this.checkOnGoing = true;
|
||||||
|
this.memberService.addGroupMember(this.projectId, this.member_group, this.role_id)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.checkOnGoing = false;
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.subscribe(
|
||||||
|
res => {
|
||||||
|
this.role_id = null;
|
||||||
|
this.addHttpAuthOpened = false;
|
||||||
|
this.added.emit(true);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
let errorMessageKey: string = errorHandFn(err);
|
||||||
|
this.translateService
|
||||||
|
.get(errorMessageKey)
|
||||||
|
.subscribe(errorMessage => this.inlineAlert.showInlineError(errorMessage));
|
||||||
|
this.added.emit(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onSubmit(): void {
|
||||||
|
this.createGroupAsMember();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.role_id = null;
|
||||||
|
this.addHttpAuthOpened = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
openAddMemberModal(): void {
|
||||||
|
this.currentForm.reset();
|
||||||
|
this.addHttpAuthOpened = true;
|
||||||
|
this.role_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get isValid(): boolean {
|
||||||
|
return this.currentForm &&
|
||||||
|
this.currentForm.valid &&
|
||||||
|
!this.checkOnGoing;
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
<button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasCreateMemberPermission">
|
<button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasCreateMemberPermission">
|
||||||
<span><clr-icon shape="plus" size="16"></clr-icon> {{'MEMBER.USER' | translate }}</span>
|
<span><clr-icon shape="plus" size="16"></clr-icon> {{'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> {{'MEMBER.LDAP_GROUP' | translate}}</span>
|
<span><clr-icon shape="plus" size="16"></clr-icon> {{'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>
|
@ -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() {
|
||||||
this.addGroupComponent.open();
|
if (this.isLdapMode) {
|
||||||
|
this.addGroupComponent.open();
|
||||||
|
} else {
|
||||||
|
this.addHttpAuthGroupComponent.openAddMemberModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
addedGroup(result: boolean) {
|
addedGroup(result: boolean) {
|
||||||
this.searchMember = "";
|
this.searchMember = "";
|
||||||
@ -188,10 +199,10 @@ export class MemberComponent implements OnInit, OnDestroy {
|
|||||||
return this.memberService
|
return this.memberService
|
||||||
.changeMemberRole(projectId, member.id, roleId)
|
.changeMemberRole(projectId, member.id, roleId)
|
||||||
.pipe(map(() => this.batchChangeRoleInfos[member.id] = 'done')
|
.pipe(map(() => this.batchChangeRoleInfos[member.id] = 'done')
|
||||||
, catchError(error => {
|
, catchError(error => {
|
||||||
this.messageHandlerService.handleError(error + ": " + member.entity_name);
|
this.messageHandlerService.handleError(error + ": " + member.entity_name);
|
||||||
return observableThrowError(error);
|
return observableThrowError(error);
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preparation for members role change
|
// Preparation for members role change
|
||||||
|
@ -38,6 +38,7 @@ import { ProjectLabelComponent } from "../project/project-label/project-label.co
|
|||||||
import { HelmChartModule } from './helm-chart/helm-chart.module';
|
import { 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]
|
||||||
|
@ -1,130 +1,129 @@
|
|||||||
<clr-modal [(clrModalOpen)]="addRobotOpened"
|
<clr-modal [(clrModalOpen)]="addRobotOpened"
|
||||||
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||||
<h3 class="modal-title">{{'ROBOT_ACCOUNT.CREAT_ROBOT_ACCOUNT' | translate}}</h3>
|
<h3 class="modal-title">{{'ROBOT_ACCOUNT.CREAT_ROBOT_ACCOUNT' | translate}}</h3>
|
||||||
<inline-alert #copyAlert class="modal-title"></inline-alert>
|
<inline-alert #copyAlert class="modal-title"></inline-alert>
|
||||||
<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}}
|
||||||
</label>
|
</label>
|
||||||
<label aria-haspopup="true" role="tooltip" class="tooltip
|
<label aria-haspopup="true" role="tooltip" class="tooltip
|
||||||
tooltip-validation
|
tooltip-validation
|
||||||
tooltip-md tooltip-bottom-left" for="robot_name"
|
tooltip-md tooltip-bottom-left" for="robot_name"
|
||||||
[class.invalid]="!isRobotNameValid">
|
[class.invalid]="!isRobotNameValid">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
[(ngModel)]="robot.name"
|
[(ngModel)]="robot.name"
|
||||||
size="30" class="input-width"
|
size="30" class="input-width"
|
||||||
name="robot_name"
|
name="robot_name"
|
||||||
id="robot_name"
|
id="robot_name"
|
||||||
#robotName="ngModel"
|
#robotName="ngModel"
|
||||||
required
|
required
|
||||||
pattern='[^" ~#$%]+'
|
pattern='[^" ~#$%]+'
|
||||||
maxLengthExt="255"
|
maxLengthExt="255"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
(keyup)='handleValidation()'>
|
(keyup)='handleValidation()'>
|
||||||
<span class="tooltip-content">
|
<span class="tooltip-content">
|
||||||
{{ nameTooltipText | translate }}
|
{{ nameTooltipText | translate }}
|
||||||
</span>
|
</span>
|
||||||
</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">
|
||||||
<label class="col-md-3">
|
<div class="clr-col-3 permission">
|
||||||
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
|
<label class="col-md-3">
|
||||||
</label>
|
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
|
||||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
</label>
|
||||||
<clr-checkbox-wrapper>
|
</div>
|
||||||
<input type="checkbox" clrCheckbox [checked]="true"
|
<div class="clr-col">
|
||||||
[(ngModel)]="robot.access.isPullImage" name="isPullImage"
|
<div class="form-group padding-left-120">
|
||||||
id="permission-pull" class="clr-checkbox">
|
<label>{{'ROBOT_ACCOUNT.PERMISSIONS_IMAGE' | translate}}</label>
|
||||||
<label for="permission-pull" class="clr-control-label">
|
<div class="radio-inline">
|
||||||
{{'ROBOT_ACCOUNT.PULL_PERMISSION' | translate}}
|
<input type="radio" name="image-permission"
|
||||||
</label>
|
id="image-permission-pull"
|
||||||
</clr-checkbox-wrapper>
|
value="pull"
|
||||||
</label>
|
[(ngModel)]="imagePermission">
|
||||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
<label for="image-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
|
||||||
<clr-checkbox-wrapper>
|
</div>
|
||||||
<input type="checkbox" clrCheckbox [checked]="true"
|
<div class="radio-inline">
|
||||||
[(ngModel)]="robot.access.isPushOrPullImage" name="isPushOrPullImage"
|
<input type="radio" name="image-permission"
|
||||||
id="permission-push" class="clr-checkbox">
|
id="image-permission-push-and-pull"
|
||||||
<label for="permission-push" class="clr-control-label">
|
value="push-and-pull"
|
||||||
{{'ROBOT_ACCOUNT.PULL_PUSH_PERMISSION' | translate}}
|
[(ngModel)]="imagePermission">
|
||||||
</label>
|
<label for="image-permission-push-and-pull">{{'ROBOT_ACCOUNT.PUSH' | translate}}
|
||||||
</clr-checkbox-wrapper>
|
& {{'ROBOT_ACCOUNT.PULL' | translate}}</label>
|
||||||
</label>
|
</div>
|
||||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
</div>
|
||||||
<clr-checkbox-wrapper>
|
<div class="form-group padding-left-120">
|
||||||
<input type="checkbox" clrCheckbox [checked]="true"
|
<label>{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</label>
|
||||||
[(ngModel)]="robot.access.isPushChart" name="isPushChart"
|
<div class="checkbox-inline">
|
||||||
id="permission-push-chart" class="clr-checkbox">
|
<input type="checkbox" id="helm-permission-push"
|
||||||
<label for="permission-push-chart" class="clr-control-label">
|
[checked]="robot.access.isPushChart"
|
||||||
{{'ROBOT_ACCOUNT.PUSH_CHART_PERMISSION' | translate}}
|
[(ngModel)]="robot.access.isPushChart"
|
||||||
</label>
|
name="helm-permission">
|
||||||
</clr-checkbox-wrapper>
|
<label for="helm-permission-push">{{'ROBOT_ACCOUNT.PUSH' | translate}}</label>
|
||||||
</label>
|
</div>
|
||||||
<label class="clr-col-md-8 no-margin padding-left-0">
|
<div class="checkbox-inline">
|
||||||
<clr-checkbox-wrapper>
|
<input type="checkbox" id="helm-permission-pull"
|
||||||
<input type="checkbox" clrCheckbox [checked]="true"
|
[checked]="robot.access.isPullChart"
|
||||||
[(ngModel)]="robot.access.isPullChart" name="isPullChart"
|
[(ngModel)]="robot.access.isPullChart"
|
||||||
id="permission-pull-chart" class="clr-checkbox">
|
name="helm-permission">
|
||||||
<label for="permission-pull-chart" class="clr-control-label">
|
<label for="helm-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
|
||||||
{{'ROBOT_ACCOUNT.PULL_CHART_PERMISSION' | translate}}
|
</div>
|
||||||
</label>
|
</div>
|
||||||
</clr-checkbox-wrapper>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div class="modal-footer">
|
||||||
<div class="modal-footer">
|
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL'
|
||||||
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL'
|
| translate}}</button>
|
||||||
| translate}}</button>
|
<button type="button" [disabled]="shouldDisable" class="btn btn-primary"
|
||||||
<button type="button" [disabled]="shouldDisable" class="btn btn-primary"
|
(click)="onSubmit()">{{'BUTTON.SAVE'
|
||||||
(click)="onSubmit()">{{'BUTTON.SAVE'
|
| translate}}</button>
|
||||||
| translate}}</button>
|
</div>
|
||||||
</div>
|
|
||||||
</clr-modal>
|
</clr-modal>
|
||||||
<clr-modal [(clrModalOpen)]="copyToken" class="copy-token"
|
<clr-modal [(clrModalOpen)]="copyToken" class="copy-token"
|
||||||
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||||
<div class="modal-title">
|
<div class="modal-title">
|
||||||
<h3 class="modal-title">
|
<h3 class="modal-title">
|
||||||
<clr-icon class="alert-icon success-icon" shape="check-circle" size="50"></clr-icon>
|
<clr-icon class="alert-icon success-icon" shape="check-circle" size="50"></clr-icon>
|
||||||
{{ createSuccess | translate}}</h3>
|
{{ createSuccess | translate}}</h3>
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
<div class="alert-items">
|
<div class="alert-items">
|
||||||
<div class="alert-item static">
|
<div class="alert-item static">
|
||||||
<div class="alert-icon-wrapper">
|
<div class="alert-icon-wrapper">
|
||||||
<clr-icon class="alert-icon" shape="info-circle"></clr-icon>
|
<clr-icon class="alert-icon" shape="info-circle"></clr-icon>
|
||||||
</div>
|
</div>
|
||||||
<span class="alert-text">{{'ROBOT_ACCOUNT.ALERT_TEXT' | translate}}</span>
|
<span class="alert-text">{{'ROBOT_ACCOUNT.ALERT_TEXT' | translate}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="modal-body">
|
||||||
<div class="modal-body">
|
<section class="form-block show-info">
|
||||||
<section class="form-block show-info">
|
<div class="form-group robot-name">
|
||||||
<div class="form-group robot-name">
|
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.NAME'
|
||||||
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.NAME'
|
| translate}}</label>
|
||||||
| translate}}</label>
|
<span>{{robotAccount}}</span>
|
||||||
<span>{{robotAccount}}</span>
|
</div>
|
||||||
</div>
|
<div class="form-group robot-token">
|
||||||
<div class="form-group robot-token">
|
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.TOKEN' |
|
||||||
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.TOKEN' |
|
translate}}</label>
|
||||||
translate}}</label>
|
<hbr-copy-input (onCopySuccess)="onCpSuccess($event)"
|
||||||
<hbr-copy-input (onCopySuccess)="onCpSuccess($event)"
|
(onCopyError)="onCpError($event)" inputSize="50" headerTitle=""
|
||||||
(onCopyError)="onCpError($event)" inputSize="50" headerTitle=""
|
defaultValue="{{robotToken}}" class="copy-input"></hbr-copy-input>
|
||||||
defaultValue="{{robotToken}}" class="copy-input"></hbr-copy-input>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
|
||||||
</clr-modal>
|
</clr-modal>
|
@ -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;
|
||||||
|
}
|
@ -38,17 +38,18 @@ 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>();
|
||||||
@ViewChild("robotForm") currentForm: NgForm;
|
@ViewChild("robotForm") currentForm: NgForm;
|
||||||
@ViewChild("copyAlert") copyAlert: InlineAlertComponent;
|
@ViewChild("copyAlert") copyAlert: InlineAlertComponent;
|
||||||
constructor(
|
constructor(
|
||||||
private robotService: RobotService,
|
private robotService: RobotService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private errorHandler: ErrorHandler,
|
private errorHandler: ErrorHandler,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef,
|
||||||
private messageHandlerService: MessageHandlerService
|
private messageHandlerService: MessageHandlerService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -59,31 +60,31 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
|||||||
if (this.isRobotNameValid) {
|
if (this.isRobotNameValid) {
|
||||||
this.checkOnGoing = true;
|
this.checkOnGoing = true;
|
||||||
this.robotService
|
this.robotService
|
||||||
.listRobotAccount(this.projectId)
|
.listRobotAccount(this.projectId)
|
||||||
.pipe(
|
.pipe(
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.checkOnGoing = false;
|
this.checkOnGoing = false;
|
||||||
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
|
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
|
||||||
setTimeout(() => clearInterval(hnd), 2000);
|
setTimeout(() => clearInterval(hnd), 2000);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
response => {
|
response => {
|
||||||
if (response && response.length) {
|
if (response && response.length) {
|
||||||
if (
|
if (
|
||||||
response.find(target => {
|
response.find(target => {
|
||||||
return target.name === "robot$" + cont.value;
|
return target.name === "robot$" + cont.value;
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
this.isRobotNameValid = false;
|
this.isRobotNameValid = false;
|
||||||
this.nameTooltipText = "ROBOT_ACCOUNT.ACCOUNT_EXISTING";
|
this.nameTooltipText = "ROBOT_ACCOUNT.ACCOUNT_EXISTING";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.errorHandler.error(error);
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
},
|
|
||||||
error => {
|
|
||||||
this.errorHandler.error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
||||||
}
|
}
|
||||||
@ -116,49 +117,57 @@ 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(
|
||||||
this.projectId,
|
this.projectId,
|
||||||
this.robot,
|
this.robot,
|
||||||
this.projectName
|
this.projectName
|
||||||
)
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.isSubmitOnGoing = false;
|
this.isSubmitOnGoing = false;
|
||||||
this.robotToken = response.token;
|
this.robotToken = response.token;
|
||||||
this.robotAccount = response.name;
|
this.robotAccount = response.name;
|
||||||
this.copyToken = true;
|
this.copyToken = true;
|
||||||
this.create.emit(true);
|
this.create.emit(true);
|
||||||
this.translate
|
this.translate
|
||||||
.get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: this.robotAccount })
|
.get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: this.robotAccount })
|
||||||
.subscribe((res: string) => {
|
.subscribe((res: string) => {
|
||||||
this.createSuccess = res;
|
this.createSuccess = res;
|
||||||
});
|
});
|
||||||
this.addRobotOpened = false;
|
this.addRobotOpened = false;
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.isSubmitOnGoing = false;
|
this.isSubmitOnGoing = false;
|
||||||
this.copyAlert.showInlineError(error);
|
this.copyAlert.showInlineError(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid(): boolean {
|
isValid(): boolean {
|
||||||
return (
|
return (
|
||||||
this.currentForm &&
|
this.currentForm &&
|
||||||
this.currentForm.valid &&
|
this.currentForm.valid &&
|
||||||
!this.isSubmitOnGoing &&
|
!this.isSubmitOnGoing &&
|
||||||
this.isRobotNameValid &&
|
this.isRobotNameValid &&
|
||||||
!this.checkOnGoing
|
!this.checkOnGoing
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
get shouldDisable(): boolean {
|
get shouldDisable(): boolean {
|
||||||
if (this.robot && this.robot.access) {
|
if (this.robot && this.robot.access) {
|
||||||
return (
|
return (
|
||||||
!this.isValid() ||
|
!this.isValid() ||
|
||||||
(!this.robot.access.isPushOrPullImage && !this.robot.access.isPullImage
|
(!this.robot.access.isPushOrPullImage && !this.robot.access.isPullImage
|
||||||
&& !this.robot.access.isPullChart && !this.robot.access.isPushChart)
|
&& !this.robot.access.isPullChart && !this.robot.access.isPushChart)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,9 +189,9 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
|||||||
onCpSuccess($event: any): void {
|
onCpSuccess($event: any): void {
|
||||||
this.copyToken = false;
|
this.copyToken = false;
|
||||||
this.translate
|
this.translate
|
||||||
.get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robotAccount })
|
.get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robotAccount })
|
||||||
.subscribe((res: string) => {
|
.subscribe((res: string) => {
|
||||||
this.messageHandlerService.showSuccess(res);
|
this.messageHandlerService.showSuccess(res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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": {
|
||||||
|
@ -87,3 +87,7 @@ body {
|
|||||||
.color-red {
|
.color-red {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.datagrid-table,.datagrid-header{
|
||||||
|
position: inherit !important;
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
176
src/replication/adapter/awsecr/adapter.go
Normal file
176
src/replication/adapter/awsecr/adapter.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package awsecr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
awsecrapi "github.com/aws/aws-sdk-go/service/ecr"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||||
|
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/adapter/native"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := adp.RegisterFactory(model.RegistryTypeAwsEcr, func(registry *model.Registry) (adp.Adapter, error) {
|
||||||
|
return newAdapter(registry)
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("failed to register factory for %s: %v", model.RegistryTypeAwsEcr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("the factory for adapter %s registered", model.RegistryTypeAwsEcr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAdapter(registry *model.Registry) (*adapter, error) {
|
||||||
|
region, err := parseRegion(registry.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authorizer := NewAuth(region, registry.Credential.AccessKey, registry.Credential.AccessSecret, registry.Insecure)
|
||||||
|
dockerRegistry, err := native.NewAdapterWithCustomizedAuthorizer(registry, authorizer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &adapter{
|
||||||
|
registry: registry,
|
||||||
|
Adapter: dockerRegistry,
|
||||||
|
region: region,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRegion(url string) (string, error) {
|
||||||
|
pattern := "https://(?:api|\\d+\\.dkr)\\.ecr\\.([\\w\\-]+)\\.amazonaws\\.com"
|
||||||
|
rs := regexp.MustCompile(pattern).FindStringSubmatch(url)
|
||||||
|
if rs == nil {
|
||||||
|
return "", errors.New("Bad aws url")
|
||||||
|
}
|
||||||
|
return rs[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type adapter struct {
|
||||||
|
*native.Adapter
|
||||||
|
registry *model.Registry
|
||||||
|
region string
|
||||||
|
forceEndpoint *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*adapter) Info() (info *model.RegistryInfo, err error) {
|
||||||
|
return &model.RegistryInfo{
|
||||||
|
Type: model.RegistryTypeAwsEcr,
|
||||||
|
SupportedResourceTypes: []model.ResourceType{
|
||||||
|
model.ResourceTypeImage,
|
||||||
|
},
|
||||||
|
SupportedResourceFilters: []*model.FilterStyle{
|
||||||
|
{
|
||||||
|
Type: model.FilterTypeName,
|
||||||
|
Style: model.FilterStyleTypeText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: model.FilterTypeTag,
|
||||||
|
Style: model.FilterStyleTypeText,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SupportedTriggers: []model.TriggerType{
|
||||||
|
model.TriggerTypeManual,
|
||||||
|
model.TriggerTypeScheduled,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck checks health status of a registry
|
||||||
|
func (a *adapter) HealthCheck() (model.HealthStatus, error) {
|
||||||
|
if a.registry.Credential == nil ||
|
||||||
|
len(a.registry.Credential.AccessKey) == 0 || len(a.registry.Credential.AccessSecret) == 0 {
|
||||||
|
log.Errorf("no credential to ping registry %s", a.registry.URL)
|
||||||
|
return model.Unhealthy, nil
|
||||||
|
}
|
||||||
|
if err := a.PingGet(); err != nil {
|
||||||
|
log.Errorf("failed to ping registry %s: %v", a.registry.URL, err)
|
||||||
|
return model.Unhealthy, nil
|
||||||
|
}
|
||||||
|
return model.Healthy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareForPush nothing need to do.
|
||||||
|
func (a *adapter) PrepareForPush(resources []*model.Resource) error {
|
||||||
|
for _, resource := range resources {
|
||||||
|
if resource == nil {
|
||||||
|
return errors.New("the resource cannot be nil")
|
||||||
|
}
|
||||||
|
if resource.Metadata == nil {
|
||||||
|
return errors.New("the metadata of resource cannot be nil")
|
||||||
|
}
|
||||||
|
if resource.Metadata.Repository == nil {
|
||||||
|
return errors.New("the namespace of resource cannot be nil")
|
||||||
|
}
|
||||||
|
if len(resource.Metadata.Repository.Name) == 0 {
|
||||||
|
return errors.New("the name of the namespace cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.createRepository(resource.Metadata.Repository.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) createRepository(repository string) error {
|
||||||
|
if a.registry.Credential == nil ||
|
||||||
|
len(a.registry.Credential.AccessKey) == 0 || len(a.registry.Credential.AccessSecret) == 0 {
|
||||||
|
return errors.New("no credential ")
|
||||||
|
}
|
||||||
|
cred := credentials.NewStaticCredentials(
|
||||||
|
a.registry.Credential.AccessKey,
|
||||||
|
a.registry.Credential.AccessSecret,
|
||||||
|
"")
|
||||||
|
if a.region == "" {
|
||||||
|
return errors.New("no region parsed")
|
||||||
|
}
|
||||||
|
config := &aws.Config{
|
||||||
|
Credentials: cred,
|
||||||
|
Region: &a.region,
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Transport: registry.GetHTTPTransport(a.registry.Insecure),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if a.forceEndpoint != nil {
|
||||||
|
config.Endpoint = a.forceEndpoint
|
||||||
|
}
|
||||||
|
sess := session.Must(session.NewSession(config))
|
||||||
|
|
||||||
|
svc := awsecrapi.New(sess)
|
||||||
|
|
||||||
|
_, err := svc.CreateRepository(&awsecrapi.CreateRepositoryInput{
|
||||||
|
RepositoryName: &repository,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := err.(awserr.Error); ok {
|
||||||
|
if e.Code() == awsecrapi.ErrCodeRepositoryAlreadyExistsException {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user