merge with latest master code

This commit is contained in:
wang yan 2019-07-11 20:21:15 +08:00
commit f066d986b9
40 changed files with 963 additions and 127 deletions

View File

@ -3639,6 +3639,9 @@ definitions:
metadata:
description: The metadata of the project.
$ref: '#/definitions/ProjectMetadata'
cve_whitelist:
description: The CVE whitelist of this project.
$ref: '#/definitions/CVEWhitelist'
ProjectMetadata:
type: object
properties:

View File

@ -20,16 +20,17 @@ import (
// keys of project metadata and severity values
const (
ProMetaPublic = "public"
ProMetaEnableContentTrust = "enable_content_trust"
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
ProMetaSeverity = "severity"
ProMetaAutoScan = "auto_scan"
SeverityNone = "negligible"
SeverityLow = "low"
SeverityMedium = "medium"
SeverityHigh = "high"
SeverityCritical = "critical"
ProMetaPublic = "public"
ProMetaEnableContentTrust = "enable_content_trust"
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
ProMetaSeverity = "severity"
ProMetaAutoScan = "auto_scan"
ProMetaReuseSysCVEWhitelist = "reuse_sys_cve_whitelist"
SeverityNone = "negligible"
SeverityLow = "low"
SeverityMedium = "medium"
SeverityHigh = "high"
SeverityCritical = "critical"
)
// ProjectMetadata holds the metadata of a project.

View File

@ -36,6 +36,7 @@ type Project struct {
RepoCount int64 `orm:"-" json:"repo_count"`
ChartCount uint64 `orm:"-" json:"chart_count"`
Metadata map[string]string `orm:"-" json:"metadata"`
CVEWhitelist CVEWhitelist `orm:"-" json:"cve_whitelist"`
}
// GetMetadata ...
@ -83,6 +84,15 @@ func (p *Project) VulPrevented() bool {
return isTrue(prevent)
}
// ReuseSysCVEWhitelist ...
func (p *Project) ReuseSysCVEWhitelist() bool {
r, ok := p.GetMetadata(ProMetaReuseSysCVEWhitelist)
if !ok {
return true
}
return isTrue(r)
}
// Severity ...
func (p *Project) Severity() string {
severity, exist := p.GetMetadata(ProMetaSeverity)
@ -154,9 +164,10 @@ type BaseProjectCollection struct {
// ProjectRequest holds informations that need for creating project API
type ProjectRequest struct {
Name string `json:"project_name"`
Public *int `json:"public"` // deprecated, reserved for project creation in replication
Metadata map[string]string `json:"metadata"`
Name string `json:"project_name"`
Public *int `json:"public"` // deprecated, reserved for project creation in replication
Metadata map[string]string `json:"metadata"`
CVEWhitelist CVEWhitelist `json:"cve_whitelist"`
}
// ProjectQueryResult ...

View File

@ -0,0 +1,114 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package redis
import (
"errors"
"github.com/garyburd/redigo/redis"
"github.com/goharbor/harbor/src/common/utils"
"time"
)
var (
// ErrUnLock ...
ErrUnLock = errors.New("error to release the redis lock")
)
const (
unlockScript = `
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
`
defaultDelay = 5 * time.Second
defaultMaxRetry = 5
defaultExpiry = 600 * time.Second
)
// Mutex ...
type Mutex struct {
Conn redis.Conn
key string
value string
opts Options
}
// New ...
func New(conn redis.Conn, key, value string) *Mutex {
o := *DefaultOptions()
if value == "" {
value = utils.GenerateRandomString()
}
return &Mutex{conn, key, value, o}
}
// Require retry to require the lock
func (rm *Mutex) Require() (bool, error) {
var isRequired bool
var err error
for i := 0; i < rm.opts.maxRetry; i++ {
isRequired, err = rm.require()
if isRequired {
break
}
if err != nil || !isRequired {
time.Sleep(rm.opts.retryDelay)
}
}
return isRequired, err
}
// require get the redis lock, for details, just refer to https://redis.io/topics/distlock
func (rm *Mutex) require() (bool, error) {
reply, err := redis.String(rm.Conn.Do("SET", rm.key, rm.value, "NX", "PX", int(rm.opts.expiry/time.Millisecond)))
if err != nil {
return false, err
}
return reply == "OK", nil
}
// Free releases the lock, for details, just refer to https://redis.io/topics/distlock
func (rm *Mutex) Free() (bool, error) {
script := redis.NewScript(1, unlockScript)
resp, err := redis.Int(script.Do(rm.Conn, rm.key, rm.value))
if err != nil {
return false, err
}
if resp == 0 {
return false, ErrUnLock
}
return true, nil
}
// Options ...
type Options struct {
retryDelay time.Duration
expiry time.Duration
maxRetry int
}
// DefaultOptions ...
func DefaultOptions() *Options {
opt := &Options{
retryDelay: defaultDelay,
expiry: defaultExpiry,
maxRetry: defaultMaxRetry,
}
return opt
}

View File

@ -0,0 +1,62 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package redis
import (
"fmt"
"github.com/garyburd/redigo/redis"
"github.com/stretchr/testify/assert"
"os"
"testing"
"time"
)
const testingRedisHost = "REDIS_HOST"
func TestRedisLock(t *testing.T) {
con, err := redis.Dial(
"tcp",
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
redis.DialConnectTimeout(30*time.Second),
redis.DialReadTimeout(time.Minute+10*time.Second),
redis.DialWriteTimeout(10*time.Second),
)
assert.Nil(t, err)
defer con.Close()
rm := New(con, "test-redis-lock", "test-value")
successLock, err := rm.Require()
assert.Nil(t, err)
assert.True(t, successLock)
time.Sleep(2 * time.Second)
_, err = rm.Require()
assert.NotNil(t, err)
successUnLock, err := rm.Free()
assert.Nil(t, err)
assert.True(t, successUnLock)
}
func getRedisHost() string {
redisHost := os.Getenv(testingRedisHost)
if redisHost == "" {
redisHost = "127.0.0.1" // for local test
}
return redisHost
}

View File

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

View File

@ -17,15 +17,16 @@ package api
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"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
@ -41,11 +42,12 @@ func (sca *SysCVEWhitelistAPI) Prepare() {
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 := dao.GetSysCVEWhitelist()
l, err := sca.manager.GetSys()
if err != nil {
sca.SendInternalServerError(err)
return
@ -67,7 +69,12 @@ func (sca *SysCVEWhitelistAPI) Put() {
sca.SendBadRequestError(errors.New(msg))
return
}
if _, err := dao.UpdateCVEWhitelist(l); err != nil {
if err := sca.manager.SetSys(l); err != nil {
if whitelist.IsInvalidErr(err) {
log.Errorf("Invalid CVE whitelist: %v", err)
sca.SendBadRequestError(err)
return
}
sca.SendInternalServerError(err)
return
}

View File

@ -90,6 +90,22 @@ func TestSysCVEWhitelistAPIPut(t *testing.T) {
},
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{

View File

@ -21,6 +21,7 @@ import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
"net/http"
"net/http/httptest"
"regexp"
@ -135,7 +136,7 @@ type PolicyChecker interface {
// contentTrustEnabled returns whether a project has enabled content trust.
ContentTrustEnabled(name string) bool
// vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity.
VulnerablePolicy(name string) (bool, models.Severity)
VulnerablePolicy(name string) (bool, models.Severity, models.CVEWhitelist)
}
// PmsPolicyChecker ...
@ -154,13 +155,29 @@ func (pc PmsPolicyChecker) ContentTrustEnabled(name string) bool {
}
// VulnerablePolicy ...
func (pc PmsPolicyChecker) VulnerablePolicy(name string) (bool, models.Severity) {
func (pc PmsPolicyChecker) VulnerablePolicy(name string) (bool, models.Severity, models.CVEWhitelist) {
project, err := pc.pm.Get(name)
wl := models.CVEWhitelist{}
if err != nil {
log.Errorf("Unexpected error when getting the project, error: %v", err)
return true, models.SevUnknown
return true, models.SevUnknown, wl
}
return project.VulPrevented(), clair.ParseClairSev(project.Severity())
mgr := whitelist.NewDefaultManager()
if project.ReuseSysCVEWhitelist() {
w, err := mgr.GetSys()
if err != nil {
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
}
wl = *w
} else {
w, err := mgr.Get(project.ProjectID)
if err != nil {
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
}
wl = *w
}
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
}
// NewPMSPolicyChecker returns an instance of an pmsPolicyChecker

View File

@ -179,9 +179,10 @@ func TestPMSPolicyChecker(t *testing.T) {
Name: name,
OwnerID: 1,
Metadata: map[string]string{
models.ProMetaEnableContentTrust: "true",
models.ProMetaPreventVul: "true",
models.ProMetaSeverity: "low",
models.ProMetaEnableContentTrust: "true",
models.ProMetaPreventVul: "true",
models.ProMetaSeverity: "low",
models.ProMetaReuseSysCVEWhitelist: "false",
},
})
require.Nil(t, err)
@ -193,9 +194,10 @@ func TestPMSPolicyChecker(t *testing.T) {
contentTrustFlag := GetPolicyChecker().ContentTrustEnabled("project_for_test_get_sev_low")
assert.True(t, contentTrustFlag)
projectVulnerableEnabled, projectVulnerableSeverity := GetPolicyChecker().VulnerablePolicy("project_for_test_get_sev_low")
projectVulnerableEnabled, projectVulnerableSeverity, wl := GetPolicyChecker().VulnerablePolicy("project_for_test_get_sev_low")
assert.True(t, projectVulnerableEnabled)
assert.Equal(t, projectVulnerableSeverity, models.SevLow)
assert.Empty(t, wl.Items)
}
func TestCopyResp(t *testing.T) {

View File

@ -16,11 +16,10 @@ package vulnerable
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/middlewares/util"
"github.com/goharbor/harbor/src/pkg/scan"
"net/http"
)
@ -47,27 +46,35 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
vh.next.ServeHTTP(rw, req)
return
}
projectVulnerableEnabled, projectVulnerableSeverity := util.GetPolicyChecker().VulnerablePolicy(img.ProjectName)
projectVulnerableEnabled, projectVulnerableSeverity, wl := util.GetPolicyChecker().VulnerablePolicy(img.ProjectName)
if !projectVulnerableEnabled {
vh.next.ServeHTTP(rw, req)
return
}
overview, err := dao.GetImgScanOverview(img.Digest)
vl, err := scan.VulnListByDigest(img.Digest)
if err != nil {
log.Errorf("failed to get ImgScanOverview with repo: %s, reference: %s, digest: %s. Error: %v", img.Repository, img.Reference, img.Digest, err)
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", "Failed to get ImgScanOverview."), http.StatusPreconditionFailed)
log.Errorf("Failed to get the vulnerability list, error: %v", err)
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", "Failed to get vulnerabilities."), http.StatusPreconditionFailed)
return
}
// severity is 0 means that the image fails to scan or not scanned successfully.
if overview == nil || overview.Sev == 0 {
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", "Cannot get the image severity."), http.StatusPreconditionFailed)
return
}
imageSev := overview.Sev
if imageSev >= int(projectVulnerableSeverity) {
log.Debugf("the image severity: %q is higher then project setting: %q, failing the response.", models.Severity(imageSev), projectVulnerableSeverity)
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("The severity of vulnerability of the image: %q is equal or higher than the threshold in project setting: %q.", models.Severity(imageSev), projectVulnerableSeverity)), http.StatusPreconditionFailed)
filtered := vl.ApplyWhitelist(wl)
msg := vh.filterMsg(img, filtered)
log.Info(msg)
if int(vl.Severity()) >= int(projectVulnerableSeverity) {
log.Debugf("the image severity: %q is higher then project setting: %q, failing the response.", vl.Severity(), projectVulnerableSeverity)
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("The severity of vulnerability of the image: %q is equal or higher than the threshold in project setting: %q.", vl.Severity(), projectVulnerableSeverity)), http.StatusPreconditionFailed)
return
}
vh.next.ServeHTTP(rw, req)
}
func (vh vulnerableHandler) filterMsg(img util.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
}

View File

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

View File

@ -0,0 +1,79 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package whitelist
import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/jobservice/logger"
)
// Manager defines the interface of CVE whitelist manager, it support both system level and project level whitelists
type Manager interface {
// CreateEmpty creates empty whitelist for given project
CreateEmpty(projectID int64) error
// Set sets the whitelist for given project (create or update)
Set(projectID int64, list models.CVEWhitelist) error
// Get gets the whitelist for given project
Get(projectID int64) (*models.CVEWhitelist, error)
// SetSys sets system level whitelist
SetSys(list models.CVEWhitelist) error
// GetSys gets system level whitelist
GetSys() (*models.CVEWhitelist, error)
}
type defaultManager struct{}
// CreateEmpty creates empty whitelist for given project
func (d *defaultManager) CreateEmpty(projectID int64) error {
l := models.CVEWhitelist{
ProjectID: projectID,
}
_, err := dao.UpdateCVEWhitelist(l)
if err != nil {
logger.Errorf("Failed to create empty CVE whitelist for project: %d, error: %v", projectID, err)
}
return err
}
// Set sets the whitelist for given project (create or update)
func (d *defaultManager) Set(projectID int64, list models.CVEWhitelist) error {
list.ProjectID = projectID
if err := Validate(list); err != nil {
return err
}
_, err := dao.UpdateCVEWhitelist(list)
return err
}
// Get gets the whitelist for given project
func (d *defaultManager) Get(projectID int64) (*models.CVEWhitelist, error) {
return dao.GetCVEWhitelist(projectID)
}
// SetSys sets the system level whitelist
func (d *defaultManager) SetSys(list models.CVEWhitelist) error {
return d.Set(0, list)
}
// GetSys gets the system level whitelist
func (d *defaultManager) GetSys() (*models.CVEWhitelist, error) {
return d.Get(0)
}
// NewDefaultManager return a new instance of defaultManager
func NewDefaultManager() Manager {
return &defaultManager{}
}

View File

@ -0,0 +1,60 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package whitelist
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
"regexp"
)
type invalidErr struct {
msg string
}
func (ie *invalidErr) Error() string {
return ie.msg
}
// NewInvalidErr ...
func NewInvalidErr(s string) error {
return &invalidErr{
msg: s,
}
}
// IsInvalidErr checks if the error is an invalidErr
func IsInvalidErr(err error) bool {
_, ok := err.(*invalidErr)
return ok
}
const cveIDPattern = `^CVE-\d{4}-\d+$`
// Validate help validates the CVE whitelist, to ensure the CVE ID is valid and there's no duplication
func Validate(wl models.CVEWhitelist) error {
m := map[string]struct{}{}
re := regexp.MustCompile(cveIDPattern)
for _, it := range wl.Items {
if !re.MatchString(it.CVEID) {
return &invalidErr{fmt.Sprintf("invalid CVE ID: %s", it.CVEID)}
}
if _, ok := m[it.CVEID]; ok {
return &invalidErr{fmt.Sprintf("duplicate CVE ID in whitelist: %s", it.CVEID)}
}
m[it.CVEID] = struct{}{}
}
return nil
}

View File

@ -0,0 +1,102 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package whitelist
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"testing"
)
func TestIsInvalidErr(t *testing.T) {
cases := []struct {
instance error
expect bool
}{
{
instance: nil,
expect: false,
},
{
instance: fmt.Errorf("whatever"),
expect: false,
},
{
instance: NewInvalidErr("This is true"),
expect: true,
},
}
for n, c := range cases {
t.Logf("Executing TestIsInvalidErr case: %d\n", n)
assert.Equal(t, c.expect, IsInvalidErr(c.instance))
}
}
func TestValidate(t *testing.T) {
cases := []struct {
l models.CVEWhitelist
noError bool
}{
{
l: models.CVEWhitelist{
Items: nil,
},
noError: true,
},
{
l: models.CVEWhitelist{
Items: []models.CVEWhitelistItem{},
},
noError: true,
},
{
l: models.CVEWhitelist{
Items: []models.CVEWhitelistItem{
{CVEID: "breakit"},
},
},
noError: false,
},
{
l: models.CVEWhitelist{
Items: []models.CVEWhitelistItem{
{CVEID: "CVE-2014-456132"},
{CVEID: "CVE-2014-7654321"},
},
},
noError: true,
},
{
l: models.CVEWhitelist{
Items: []models.CVEWhitelistItem{
{CVEID: "CVE-2014-456132"},
{CVEID: "CVE-2014-456132"},
{CVEID: "CVE-2014-7654321"},
},
},
noError: false,
},
}
for n, c := range cases {
t.Logf("Executing TestValidate case: %d\n", n)
e := Validate(c.l)
assert.Equal(t, c.noError, e == nil)
if e != nil {
assert.True(t, IsInvalidErr(e))
}
}
}

View File

@ -62,8 +62,8 @@
<span class="spinner spinner-inline spinner-position" [hidden]="onGoing === false"></span>
<div formArrayName="filters">
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
<div [formGroupName]="i">
<div class="width-70">
<div [formGroupName]="i" *ngIf="supportedFilters[i]?.type !=='label' || (supportedFilters[i]?.type==='label' && supportedFilterLabels?.length)">
<div class="width-70" >
<label>{{"REPLICATION." + supportedFilters[i]?.type.toUpperCase() | translate}}:</label>
</div>
<label *ngIf="supportedFilters[i]?.style==='input'" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"

View File

@ -32,6 +32,7 @@ import { ErrorHandler } from "../error-handler/error-handler";
import { TranslateService } from "@ngx-translate/core";
import { EndpointService } from "../service/endpoint.service";
import { cronRegex } from "../utils";
import { FilterType } from "../shared/shared.const";
@Component({
@ -265,14 +266,13 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
get filters(): FormArray {
console.log(this.ruleForm.get("filters"));
return this.ruleForm.get("filters") as FormArray;
}
setFilter(filters: Filter[]) {
const filterFGs = filters.map(filter => {
if (filter.type === 'label') {
if (filter.type === FilterType.LABEL) {
let fbLabel = this.fb.group({
type: 'label'
type: FilterType.LABEL
});
let filterLabel = this.fb.array(filter.value);
fbLabel.setControl('value', filterLabel);
@ -286,7 +286,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
initFilter(name: string) {
if (name === 'label') {
if (name === FilterType.LABEL) {
const labelArray = this.fb.array([]);
const labelControl = this.fb.group({type: name});
labelControl.setControl('value', labelArray);
@ -439,7 +439,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.ruleForm.get("trigger").get("type").setValue(this.supportedTriggers[0]);
}
getLabelListFromAdapter(supportedFilter) {
if (supportedFilter.type === 'label') {
if (supportedFilter.type === FilterType.LABEL && supportedFilter.values) {
this.supportedFilterLabels = [];
supportedFilter.values.forEach( value => {
this.supportedFilterLabels.push({
@ -453,7 +453,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
getLabelListFromRuleInfo(ruleInfo) {
let labelValueObj = ruleInfo.filters.find((currentValue) => {
return currentValue.type === 'label';
return currentValue.type === FilterType.LABEL;
});
if (labelValueObj) {
for (const labelValue of labelValueObj.value) {
@ -533,7 +533,11 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
if (!findTag) {
filtersArray.push({ type: this.supportedFilters[i].type, value: "" });
if (this.supportedFilters[i].type === FilterType.LABEL) {
filtersArray.push({ type: this.supportedFilters[i].type, value: [] });
} else {
filtersArray.push({ type: this.supportedFilters[i].type, value: "" });
}
}
}

View File

@ -434,3 +434,8 @@ export interface HttpOptionTextInterface {
withCredentials?: boolean;
}
export interface ProjectRootInterface {
NAME: string;
VALUE: number;
LABEL: string;
}

View File

@ -61,6 +61,12 @@ export const CommonRoutes = {
export const enum ConfirmationState {
NA, CONFIRMED, CANCEL
}
export const FilterType = {
NAME: "name",
TAG: "tag",
LABEL: "label",
RESOURCE: "resource"
};
export const enum ConfirmationButtons {
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE, REPLICATE_CANCEL, STOP_CANCEL
@ -84,3 +90,35 @@ export const LabelColor = [
{ 'color': '#F52F52', 'textColor': 'black' }, { 'color': '#FF5501', 'textColor': 'black' },
{ 'color': '#F57600', 'textColor': 'black' }, { 'color': '#FFDC0B', 'textColor': 'black' },
];
export const CONFIG_AUTH_MODE = {
HTTP_AUTH: "http_auth",
LDAP_AUTH: "ldap_auth"
};
export const PROJECT_ROOTS = [
{
NAME: "admin",
VALUE: 1,
LABEL: "GROUP.PROJECT_ADMIN"
},
{
NAME: "master",
VALUE: 4,
LABEL: "GROUP.PROJECT_MASTER"
},
{
NAME: "developer",
VALUE: 2,
LABEL: "GROUP.DEVELOPER"
},
{
NAME: "guest",
VALUE: 3,
LABEL: "GROUP.GUEST"
}
];
export enum GroupType {
LDAP_TYPE = 1,
HTTP_TYPE = 2
}

View File

@ -19,7 +19,7 @@ import { CookieService } from 'ngx-cookie';
import { AppConfig } from './app-config';
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
import { maintainUrlQueryParmas } from './shared/shared.utils';
import { HTTP_GET_OPTIONS} from '@harbor/ui';
import { HTTP_GET_OPTIONS , CONFIG_AUTH_MODE} from '@harbor/ui';
import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs";
export const systemInfoEndpoint = "/api/systeminfo";
@ -67,7 +67,10 @@ export class AppConfigService {
}
public isLdapMode(): boolean {
return this.configurations && this.configurations.auth_mode === 'ldap_auth';
return this.configurations && this.configurations.auth_mode === CONFIG_AUTH_MODE.LDAP_AUTH;
}
public isHttpAuthMode(): boolean {
return this.configurations && this.configurations.auth_mode === CONFIG_AUTH_MODE.HTTP_AUTH;
}
// Return the reconstructed admiral url

View File

@ -28,7 +28,7 @@
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
</a>
<a *ngIf='isLdapMode' clrVerticalNavLink routerLink="/harbor/groups" routerLinkActive="active">
<a *ngIf='isLdapMode || isHttpAuthMode' clrVerticalNavLink routerLink="/harbor/groups" routerLinkActive="active">
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}}
</a>

View File

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

View File

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

View File

@ -1,13 +1,15 @@
import {finalize} from 'rxjs/operators';
import { finalize } from 'rxjs/operators';
import { Subscription } from "rxjs";
import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms";
import { GroupType } from "@harbor/ui";
import { GroupService } from "../group.service";
import { MessageHandlerService } from "./../../shared/message-handler/message-handler.service";
import { SessionService } from "./../../shared/session.service";
import { UserGroup } from "./../group";
import { AppConfigService } from "../../app-config.service";
@Component({
selector: "hbr-add-group-modal",
@ -19,7 +21,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
mode = "create";
dnTooltip = 'TOOLTIP.ITEM_REQUIRED';
group: UserGroup = new UserGroup();
group: UserGroup;
formChangeSubscription: Subscription;
@ -30,25 +32,36 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
@Output() dataChange = new EventEmitter();
isLdapMode: boolean;
isHttpAuthMode: boolean;
constructor(
private session: SessionService,
private msgHandler: MessageHandlerService,
private appConfigService: AppConfigService,
private groupService: GroupService,
private cdr: ChangeDetectorRef
) {}
) { }
ngOnInit() { }
ngOnInit() {
if (this.appConfigService.isLdapMode()) {
this.isLdapMode = true;
}
if (this.appConfigService.isHttpAuthMode()) {
this.isHttpAuthMode = true;
}
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : GroupType.HTTP_TYPE);
}
ngOnDestroy() { }
public get isDNInvalid(): boolean {
let dnControl = this.groupForm.controls['ldap_group_dn'];
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
}
public get isNameInvalid(): boolean {
let dnControl = this.groupForm.controls['group_name'];
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
}
public get isFormValid(): boolean {
@ -83,7 +96,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
let groupCopy = Object.assign({}, this.group);
this.groupService
.createGroup(groupCopy).pipe(
finalize(() => this.close()))
finalize(() => this.close()))
.subscribe(
res => {
this.msgHandler.showSuccess("GROUP.ADD_GROUP_SUCCESS");
@ -97,7 +110,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
let groupCopy = Object.assign({}, this.group);
this.groupService
.editGroup(groupCopy).pipe(
finalize(() => this.close()))
finalize(() => this.close()))
.subscribe(
res => {
this.msgHandler.showSuccess("GROUP.EDIT_GROUP_SUCCESS");
@ -108,7 +121,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
}
resetGroup() {
this.group = new UserGroup();
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : GroupType.HTTP_TYPE);
this.groupForm.reset();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,36 @@
<clr-modal [(clrModalOpen)]="addHttpAuthOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'GROUP.NEW_MEMBER' | translate}}</h3>
<inline-alert class="modal-title padding-0"></inline-alert>
<div class="modal-body">
<label>{{ 'GROUP.NEW_USER_INFO' | translate}}</label>
<form #memberForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="member_name" class="col-md-4 form-group-label-override required">{{'GROUP.GROUP' | translate}} {{'GROUP.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="member_name" [(ngModel)]="member_group.group_name"
name="member_name"
size="20"
minlength="3"
#memberName="ngModel"
required autocomplete="off">
</label>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div>
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'GROUP.ROLE' | translate}}</label>
<div class="radio" *ngFor="let projectRoot of projectRoots">
<input type="radio" name="member_role" id="{{'check_root_project_' + projectRoot.NAME}}" [value]="projectRoot.VALUE" [(ngModel)]="role_id">
<label for="{{'check_root_project_' + projectRoot.NAME}}">{{ projectRoot.LABEL | translate}}</label>
</div>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

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

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AddHttpAuthGroupComponent } from './add-http-auth-group.component';
describe('AddHttpAuthGroupComponent', () => {
let component: AddHttpAuthGroupComponent;
let fixture: ComponentFixture<AddHttpAuthGroupComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddHttpAuthGroupComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddHttpAuthGroupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,120 @@
import { finalize } from 'rxjs/operators';
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
Component,
Input,
EventEmitter,
Output,
ViewChild,
OnInit
} from '@angular/core';
import { NgForm } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { InlineAlertComponent } from '../../../shared/inline-alert/inline-alert.component';
import { UserService } from '../../../user/user.service';
import { errorHandler as errorHandFn, PROJECT_ROOTS, ProjectRootInterface } from "@harbor/ui";
import { MemberService } from '../member.service';
import { UserGroup } from "./../../../group/group";
@Component({
selector: 'add-http-auth-group',
templateUrl: './add-http-auth-group.component.html',
styleUrls: ['./add-http-auth-group.component.scss'],
providers: [UserService]
})
export class AddHttpAuthGroupComponent implements OnInit {
projectRoots: ProjectRootInterface[];
member_group: UserGroup = { group_name: '', group_type: 2 };
role_id: number;
addHttpAuthOpened: boolean;
memberForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('memberForm')
currentForm: NgForm;
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
@Input() projectId: number;
@Output() added = new EventEmitter<boolean>();
checkOnGoing: boolean = false;
constructor(private memberService: MemberService,
private translateService: TranslateService) { }
ngOnInit(): void {
this.projectRoots = PROJECT_ROOTS;
}
createGroupAsMember() {
this.checkOnGoing = true;
this.memberService.addGroupMember(this.projectId, this.member_group, this.role_id)
.pipe(
finalize(() => {
this.checkOnGoing = false;
}
))
.subscribe(
res => {
this.role_id = null;
this.addHttpAuthOpened = false;
this.added.emit(true);
},
err => {
let errorMessageKey: string = errorHandFn(err);
this.translateService
.get(errorMessageKey)
.subscribe(errorMessage => this.inlineAlert.showInlineError(errorMessage));
this.added.emit(false);
}
);
}
onSubmit(): void {
this.createGroupAsMember();
}
onCancel() {
this.role_id = null;
this.addHttpAuthOpened = false;
}
openAddMemberModal(): void {
this.currentForm.reset();
this.addHttpAuthOpened = true;
this.role_id = 1;
}
public get isValid(): boolean {
return this.currentForm &&
this.currentForm.valid &&
!this.checkOnGoing;
}
}

View File

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

View File

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

View File

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

View File

@ -328,6 +328,7 @@
"GROUP": "Group",
"GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "New Group",
"EDIT": "Edit",
"DELETE": "Delete",
@ -340,8 +341,17 @@
"ADD_GROUP_SUCCESS": "Add group success",
"EDIT_GROUP_SUCCESS": "Edit group success",
"LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "of",
"ITEMS": "items"
"ITEMS": "items",
"NEW_MEMBER": "New Group Member",
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
"ROLE": "Role",
"SYS_ADMIN": "System Admin",
"PROJECT_ADMIN": "Project Admin",
"PROJECT_MASTER": "Master",
"DEVELOPER": "Developer",
"GUEST": "Guest"
},
"AUDIT_LOG": {
"USERNAME": "Username",

View File

@ -329,6 +329,7 @@
"GROUP": "Group",
"GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "Add",
"EDIT": "Edit",
"DELETE": "Delete",
@ -340,8 +341,17 @@
"ADD_GROUP_SUCCESS": "Add group success",
"EDIT_GROUP_SUCCESS": "Edit group success",
"LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "of",
"ITEMS": "items"
"ITEMS": "items",
"NEW_MEMBER": "New Group Member",
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
"ROLE": "Role",
"SYS_ADMIN": "System Admin",
"PROJECT_ADMIN": "Project Admin",
"PROJECT_MASTER": "Master",
"DEVELOPER": "Developer",
"GUEST": "Guest"
},
"AUDIT_LOG": {
"USERNAME": "Nombre de usuario",

View File

@ -321,6 +321,7 @@
"Group": "Group",
"GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "Add",
"EDIT": "Edit",
"DELETE": "Delete",
@ -333,8 +334,17 @@
"ADD_GROUP_SUCCESS": "Add group success",
"EDIT_GROUP_SUCCESS": "Edit group success",
"LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "of",
"ITEMS": "items"
"ITEMS": "items",
"NEW_MEMBER": "New Group Member",
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
"ROLE": "Role",
"SYS_ADMIN": "System Admin",
"PROJECT_ADMIN": "Project Admin",
"PROJECT_MASTER": "Master",
"DEVELOPER": "Developer",
"GUEST": "Guest"
},
"AUDIT_LOG": {
"USERNAME": "Nom d'utilisateur",

View File

@ -326,6 +326,7 @@
"GROUP": "Grupo",
"GROUPS": "Grupos",
"IMPORT_LDAP_GROUP": "Importar grupo do LDAP",
"IMPORT_HTTP_GROUP": "New HTTP Group",
"ADD": "Novo Grupo",
"EDIT": "Editar",
"DELETE": "Remover",
@ -338,8 +339,17 @@
"ADD_GROUP_SUCCESS": "Grupo adicionado com sucesso",
"EDIT_GROUP_SUCCESS": "Grupo editado com sucesso",
"LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "de",
"ITEMS": "itens"
"ITEMS": "itens",
"NEW_MEMBER": "New Group Member",
"NEW_USER_INFO": "Add a group to be a member of this project with specified role",
"ROLE": "Role",
"SYS_ADMIN": "System Admin",
"PROJECT_ADMIN": "Project Admin",
"PROJECT_MASTER": "Master",
"DEVELOPER": "Developer",
"GUEST": "Guest"
},
"AUDIT_LOG": {
"USERNAME": "Nome do usuário",

View File

@ -327,6 +327,7 @@
"GROUP": "组",
"GROUPS": "组",
"IMPORT_LDAP_GROUP": "导入LDAP组",
"IMPORT_HTTP_GROUP": "新建HTTP组",
"ADD": "新增",
"EDIT": "编辑",
"DELETE": "删除",
@ -339,8 +340,17 @@
"ADD_GROUP_SUCCESS": "添加组成功",
"EDIT_GROUP_SUCCESS": "修改组成功",
"LDAP_TYPE": "LDAP",
"HTTP_TYPE": "HTTP",
"OF": "共计",
"ITEMS": "条记录"
"ITEMS": "条记录",
"NEW_MEMBER": "新建组成员",
"NEW_USER_INFO": "添加一个组作为具有指定角色的此项目的成员",
"ROLE": "权限",
"SYS_ADMIN": "系统管理员",
"PROJECT_ADMIN": "项目管理员",
"PROJECT_MASTER": "维护人员",
"DEVELOPER": "开发者",
"GUEST": "访客"
},
"AUDIT_LOG": {
"USERNAME": "用户名",