mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-18 13:41:21 +01:00
Merge branch 'master' into feature/tag_retention
This commit is contained in:
commit
746d082e2e
2
Makefile
2
Makefile
@ -107,7 +107,7 @@ REDISVERSION=$(VERSIONTAG)
|
||||
NOTARYMIGRATEVERSION=v3.5.4
|
||||
|
||||
# version of chartmuseum
|
||||
CHARTMUSEUMVERSION=v0.8.1
|
||||
CHARTMUSEUMVERSION=v0.9.0
|
||||
|
||||
define VERSIONS_FOR_PREPARE
|
||||
VERSION_TAG: $(VERSIONTAG)
|
||||
|
@ -516,7 +516,7 @@ paths:
|
||||
'403':
|
||||
description: User in session does not have permission to the project.
|
||||
'409':
|
||||
description: An LDAP user group with same DN already exist.
|
||||
description: A user group with same group name already exist or an LDAP user group with same DN already exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/projects/{project_id}/members/{mid}':
|
||||
@ -2575,7 +2575,7 @@ paths:
|
||||
'403':
|
||||
description: User in session does not have permission to the user group.
|
||||
'409':
|
||||
description: An LDAP user group with same DN already exist.
|
||||
description: A user group with same group name already exist, or an LDAP user group with same DN already exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/usergroups/{group_id}':
|
||||
@ -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:
|
||||
@ -4584,7 +4587,7 @@ definitions:
|
||||
description: The name of the user group
|
||||
group_type:
|
||||
type: integer
|
||||
description: 'The group type, 1 for LDAP group.'
|
||||
description: 'The group type, 1 for LDAP group, 2 for HTTP group.'
|
||||
ldap_group_dn:
|
||||
type: string
|
||||
description: The DN of the LDAP group if group type is 1 (LDAP group).
|
||||
|
@ -72,14 +72,25 @@ chart:
|
||||
log:
|
||||
# options are debug, info, warning, error, fatal
|
||||
level: info
|
||||
# Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.
|
||||
rotate_count: 50
|
||||
# Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.
|
||||
# If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G
|
||||
# are all valid.
|
||||
rotate_size: 200M
|
||||
# The directory on your host that store log
|
||||
location: /var/log/harbor
|
||||
# configs for logs in local storage
|
||||
local:
|
||||
# Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.
|
||||
rotate_count: 50
|
||||
# Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.
|
||||
# If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G
|
||||
# are all valid.
|
||||
rotate_size: 200M
|
||||
# The directory on your host that store log
|
||||
location: /var/log/harbor
|
||||
|
||||
# Uncomment following lines to enable external syslog endpoint.
|
||||
# external_endpoint:
|
||||
# # protocol used to transmit log to external endpoint, options is tcp or udp
|
||||
# protocol: tcp
|
||||
# # The host of external endpoint
|
||||
# host: localhost
|
||||
# # Port of external endpoint
|
||||
# port: 5140
|
||||
|
||||
#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
|
||||
_version: 1.8.0
|
||||
|
25
make/migrations/postgresql/0005_1.8.2_schema.up.sql
Normal file
25
make/migrations/postgresql/0005_1.8.2_schema.up.sql
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
Rename the duplicate names before adding "UNIQUE" constraint
|
||||
*/
|
||||
DO $$
|
||||
BEGIN
|
||||
WHILE EXISTS (SELECT count(*) FROM user_group GROUP BY group_name HAVING count(*) > 1) LOOP
|
||||
UPDATE user_group AS r
|
||||
SET group_name = (
|
||||
/*
|
||||
truncate the name if it is too long after appending the sequence number
|
||||
*/
|
||||
CASE WHEN (length(group_name)+length(v.seq::text)+1) > 256
|
||||
THEN
|
||||
substring(group_name from 1 for (255-length(v.seq::text))) || '_' || v.seq
|
||||
ELSE
|
||||
group_name || '_' || v.seq
|
||||
END
|
||||
)
|
||||
FROM (SELECT id, row_number() OVER (PARTITION BY group_name ORDER BY id) AS seq FROM user_group) AS v
|
||||
WHERE r.id = v.id AND v.seq > 1;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE user_group ADD CONSTRAINT unique_group_name UNIQUE (group_name);
|
||||
|
@ -4,7 +4,7 @@ set +e
|
||||
|
||||
usage(){
|
||||
echo "Usage: builder <golang image:version> <code path> <code release tag> <main.go path> <binary name>"
|
||||
echo "e.g: builder golang:1.11.2 github.com/helm/chartmuseum v0.8.1 cmd/chartmuseum chartm"
|
||||
echo "e.g: builder golang:1.11.2 github.com/helm/chartmuseum v0.9.0 cmd/chartmuseum chartm"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ if [ $# != 5 ]; then
|
||||
fi
|
||||
|
||||
GOLANG_IMAGE="$1"
|
||||
CODE_PATH="$2"
|
||||
GIT_PATH="$2"
|
||||
CODE_VERSION="$3"
|
||||
MAIN_GO_PATH="$4"
|
||||
BIN_NAME="$5"
|
||||
@ -27,7 +27,7 @@ mkdir -p binary
|
||||
rm -rf binary/$BIN_NAME || true
|
||||
cp compile.sh binary/
|
||||
|
||||
docker run -it -v $cur/binary:/go/bin --name golang_code_builder $GOLANG_IMAGE /bin/bash /go/bin/compile.sh $CODE_PATH $CODE_VERSION $MAIN_GO_PATH $BIN_NAME
|
||||
docker run -it --rm -v $cur/binary:/go/bin --name golang_code_builder $GOLANG_IMAGE /bin/bash /go/bin/compile.sh $GIT_PATH $CODE_VERSION $MAIN_GO_PATH $BIN_NAME
|
||||
|
||||
#Clear
|
||||
docker rm -f golang_code_builder
|
||||
|
@ -11,24 +11,21 @@ if [ $# != 4 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
CODE_PATH="$1"
|
||||
GIT_PATH="$1"
|
||||
VERSION="$2"
|
||||
MAIN_GO_PATH="$3"
|
||||
BIN_NAME="$4"
|
||||
|
||||
#Get the source code of chartmusem
|
||||
go get $CODE_PATH
|
||||
|
||||
#Get the source code
|
||||
git clone $GIT_PATH src_code
|
||||
ls
|
||||
SRC_PATH=$(pwd)/src_code
|
||||
set -e
|
||||
|
||||
#Checkout the released tag branch
|
||||
cd /go/src/$CODE_PATH
|
||||
git checkout tags/$VERSION -b $VERSION
|
||||
|
||||
#Install the go dep tool to restore the package dependencies
|
||||
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
dep ensure
|
||||
cd $SRC_PATH
|
||||
git checkout tags/$VERSION -b $VERSION
|
||||
|
||||
#Compile
|
||||
cd /go/src/$CODE_PATH/$MAIN_GO_PATH && go build -a -o $BIN_NAME
|
||||
cd $SRC_PATH/$MAIN_GO_PATH && go build -a -o $BIN_NAME
|
||||
mv $BIN_NAME /go/bin/
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Rsyslog configuration file for docker.
|
||||
|
||||
template(name="DynaFile" type="string"
|
||||
string="/var/log/docker/%syslogtag:R,ERE,0,DFLT:[^[]*--end:secpath-replace%.log"
|
||||
)
|
||||
#if $programname == "docker" then ?DynaFile
|
||||
if $programname != "rsyslogd" then -?DynaFile
|
||||
|
||||
template(name="DynaFile" type="string" string="/var/log/docker/%programname%.log")
|
||||
if $programname != "rsyslogd" then {
|
||||
action(type="omfile" dynaFile="DynaFile")
|
||||
}
|
||||
|
@ -14,7 +14,8 @@ services:
|
||||
- SETUID
|
||||
volumes:
|
||||
- {{log_location}}/:/var/log/docker/:z
|
||||
- ./common/config/log/:/etc/logrotate.d/:z
|
||||
- ./common/config/log/logrotate.conf:/etc/logrotate.d/logrotate.conf:z
|
||||
- ./common/config/log/rsyslog_docker.conf:/etc/rsyslog.d/rsyslog_docker.conf:z
|
||||
ports:
|
||||
- 127.0.0.1:1514:10514
|
||||
networks:
|
||||
|
11
make/photon/prepare/templates/log/rsyslog_docker.conf.jinja
Normal file
11
make/photon/prepare/templates/log/rsyslog_docker.conf.jinja
Normal file
@ -0,0 +1,11 @@
|
||||
# Rsyslog configuration file for docker.
|
||||
|
||||
template(name="DynaFile" type="string" string="/var/log/docker/%programname%.log")
|
||||
|
||||
if $programname != "rsyslogd" then {
|
||||
{%if log_external %}
|
||||
action(type="omfwd" Target="{{log_ep_host}}" Port="{{log_ep_port}}" Protocol="{{log_ep_protocol}}" Template="RSYSLOG_SyslogProtocol23Format")
|
||||
{% else %}
|
||||
action(type="omfile" dynaFile="DynaFile")
|
||||
{% endif %}
|
||||
}
|
@ -13,6 +13,14 @@ def validate(conf, **kwargs):
|
||||
if not conf.get("cert_key_path"):
|
||||
raise Exception("Error: The protocol is https but attribute ssl_cert_key is not set")
|
||||
|
||||
# log endpoint validate
|
||||
if ('log_ep_host' in conf) and not conf['log_ep_host']:
|
||||
raise Exception('Error: must set log endpoint host to enable external host')
|
||||
if ('log_ep_port' in conf) and not conf['log_ep_port']:
|
||||
raise Exception('Error: must set log endpoint port to enable external host')
|
||||
if ('log_ep_protocol' in conf) and (conf['log_ep_protocol'] not in ['udp', 'tcp']):
|
||||
raise Exception("Protocol in external log endpoint must be one of 'udp' or 'tcp' ")
|
||||
|
||||
# Storage validate
|
||||
valid_storage_drivers = ["filesystem", "azure", "gcs", "s3", "swift", "oss"]
|
||||
storage_provider_name = conf.get("storage_provider_name")
|
||||
@ -183,14 +191,27 @@ def parse_yaml_config(config_file_path):
|
||||
# Log configs
|
||||
allowed_levels = ['debug', 'info', 'warning', 'error', 'fatal']
|
||||
log_configs = configs.get('log') or {}
|
||||
config_dict['log_location'] = log_configs["location"]
|
||||
config_dict['log_rotate_count'] = log_configs["rotate_count"]
|
||||
config_dict['log_rotate_size'] = log_configs["rotate_size"]
|
||||
|
||||
log_level = log_configs['level']
|
||||
if log_level not in allowed_levels:
|
||||
raise Exception('log level must be one of debug, info, warning, error, fatal')
|
||||
config_dict['log_level'] = log_level.lower()
|
||||
|
||||
# parse local log related configs
|
||||
local_logs = log_configs.get('local') or {}
|
||||
if local_logs:
|
||||
config_dict['log_location'] = local_logs.get('location') or '/var/log/harbor'
|
||||
config_dict['log_rotate_count'] = local_logs.get('rotate_count') or 50
|
||||
config_dict['log_rotate_size'] = local_logs.get('rotate_size') or '200M'
|
||||
|
||||
# parse external log endpoint related configs
|
||||
if log_configs.get('external_endpoint'):
|
||||
config_dict['log_external'] = True
|
||||
config_dict['log_ep_protocol'] = log_configs['external_endpoint']['protocol']
|
||||
config_dict['log_ep_host'] = log_configs['external_endpoint']['host']
|
||||
config_dict['log_ep_port'] = log_configs['external_endpoint']['port']
|
||||
else:
|
||||
config_dict['log_external'] = False
|
||||
|
||||
# external DB, optional, if external_db enabled, it will cover the database config
|
||||
external_db_configs = configs.get('external_database') or {}
|
||||
@ -202,7 +223,7 @@ def parse_yaml_config(config_file_path):
|
||||
config_dict['harbor_db_username'] = external_db_configs['harbor']['username']
|
||||
config_dict['harbor_db_password'] = external_db_configs['harbor']['password']
|
||||
config_dict['harbor_db_sslmode'] = external_db_configs['harbor']['ssl_mode']
|
||||
# clari db
|
||||
# clair db
|
||||
config_dict['clair_db_host'] = external_db_configs['clair']['host']
|
||||
config_dict['clair_db_port'] = external_db_configs['clair']['port']
|
||||
config_dict['clair_db_name'] = external_db_configs['clair']['db_name']
|
||||
|
@ -14,7 +14,7 @@ def prepare_docker_compose(configs, with_clair, with_notary, with_chartmuseum):
|
||||
REGISTRY_VERSION = versions.get('REGISTRY_VERSION') or 'v2.7.1'
|
||||
NOTARY_VERSION = versions.get('NOTARY_VERSION') or 'v0.6.1'
|
||||
CLAIR_VERSION = versions.get('CLAIR_VERSION') or 'v2.0.7'
|
||||
CHARTMUSEUM_VERSION = versions.get('CHARTMUSEUM_VERSION') or 'v0.8.1'
|
||||
CHARTMUSEUM_VERSION = versions.get('CHARTMUSEUM_VERSION') or 'v0.9.0'
|
||||
|
||||
rendering_variables = {
|
||||
'version': VERSION_TAG,
|
||||
@ -33,17 +33,25 @@ def prepare_docker_compose(configs, with_clair, with_notary, with_chartmuseum):
|
||||
'with_chartmuseum': with_chartmuseum
|
||||
}
|
||||
|
||||
# for gcs
|
||||
storage_config = configs.get('storage_provider_config') or {}
|
||||
if storage_config.get('keyfile') and configs['storage_provider_name'] == 'gcs':
|
||||
rendering_variables['gcs_keyfile'] = storage_config['keyfile']
|
||||
|
||||
# for http
|
||||
if configs['protocol'] == 'https':
|
||||
rendering_variables['cert_key_path'] = configs['cert_key_path']
|
||||
rendering_variables['cert_path'] = configs['cert_path']
|
||||
rendering_variables['https_port'] = configs['https_port']
|
||||
|
||||
# for uaa
|
||||
uaa_config = configs.get('uaa') or {}
|
||||
if uaa_config.get('ca_file'):
|
||||
rendering_variables['uaa_ca_file'] = uaa_config['ca_file']
|
||||
|
||||
# for log
|
||||
log_ep_host = configs.get('log_ep_host')
|
||||
if log_ep_host:
|
||||
rendering_variables['external_log_endpoint'] = True
|
||||
|
||||
render_jinja(docker_compose_template_path, docker_compose_yml_path, **rendering_variables)
|
@ -5,9 +5,15 @@ from utils.misc import prepare_config_dir
|
||||
from utils.jinja import render_jinja
|
||||
|
||||
log_config_dir = os.path.join(config_dir, "log")
|
||||
|
||||
# logrotate config file
|
||||
logrotate_template_path = os.path.join(templates_dir, "log", "logrotate.conf.jinja")
|
||||
log_rotate_config = os.path.join(config_dir, "log", "logrotate.conf")
|
||||
|
||||
# syslog docker config file
|
||||
log_syslog_docker_template_path = os.path.join(templates_dir, 'log', 'rsyslog_docker.conf.jinja')
|
||||
log_syslog_docker_config = os.path.join(config_dir, 'log', 'rsyslog_docker.conf')
|
||||
|
||||
def prepare_log_configs(config_dict):
|
||||
prepare_config_dir(log_config_dir)
|
||||
|
||||
@ -17,4 +23,13 @@ def prepare_log_configs(config_dict):
|
||||
log_rotate_config,
|
||||
uid=DEFAULT_UID,
|
||||
gid=DEFAULT_GID,
|
||||
**config_dict)
|
||||
**config_dict)
|
||||
|
||||
# Render syslog docker config
|
||||
render_jinja(
|
||||
log_syslog_docker_template_path,
|
||||
log_syslog_docker_config,
|
||||
uid=DEFAULT_UID,
|
||||
gid=DEFAULT_GID,
|
||||
**config_dict
|
||||
)
|
10
make/prepare
10
make/prepare
@ -35,7 +35,7 @@ set -e
|
||||
# Copy harbor.yml to input dir
|
||||
if [[ ! "$1" =~ ^\-\- ]] && [ -f "$1" ]
|
||||
then
|
||||
cp $1 $input_dir/harbor.yml
|
||||
cp $1 $input_dir/harbor.yml
|
||||
else
|
||||
cp ${harbor_prepare_path}/harbor.yml $input_dir/harbor.yml
|
||||
fi
|
||||
@ -45,10 +45,10 @@ secret_dir=${data_path}/secret
|
||||
config_dir=$harbor_prepare_path/common/config
|
||||
|
||||
# Run prepare script
|
||||
docker run --rm -v $input_dir:/input \
|
||||
-v $harbor_prepare_path:/compose_location \
|
||||
-v $config_dir:/config \
|
||||
-v $secret_dir:/secret \
|
||||
docker run --rm -v $input_dir:/input:z \
|
||||
-v $harbor_prepare_path:/compose_location:z \
|
||||
-v $config_dir:/config:z \
|
||||
-v $secret_dir:/secret:z \
|
||||
goharbor/prepare:dev $@
|
||||
|
||||
echo "Clean up the input dir"
|
||||
|
@ -91,7 +91,7 @@ var (
|
||||
{Name: common.LDAPBaseDN, Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_BASE_DN", DefaultValue: "", ItemType: &NonEmptyStringType{}, Editable: false},
|
||||
{Name: common.LDAPFilter, Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_FILTER", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: common.LDAPGroupBaseDN, Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_BASE_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: common.LdapGroupAdminDn, Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_ADMIN_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: common.LDAPGroupAdminDn, Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_ADMIN_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: common.LDAPGroupAttributeName, Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_GID", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: common.LDAPGroupSearchFilter, Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_FILTER", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: common.LDAPGroupSearchScope, Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_SCOPE", DefaultValue: "2", ItemType: &LdapScopeType{}, Editable: false},
|
||||
@ -133,7 +133,7 @@ var (
|
||||
{Name: common.HTTPAuthProxyEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
|
||||
{Name: common.HTTPAuthProxyTokenReviewEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
|
||||
{Name: common.HTTPAuthProxyVerifyCert, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "true", ItemType: &BoolType{}},
|
||||
{Name: common.HTTPAuthProxyAlwaysOnboard, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},
|
||||
{Name: common.HTTPAuthProxySkipSearch, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},
|
||||
|
||||
{Name: common.OIDCName, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
|
||||
{Name: common.OIDCEndpoint, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
|
||||
|
@ -100,7 +100,7 @@ const (
|
||||
HTTPAuthProxyEndpoint = "http_authproxy_endpoint"
|
||||
HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint"
|
||||
HTTPAuthProxyVerifyCert = "http_authproxy_verify_cert"
|
||||
HTTPAuthProxyAlwaysOnboard = "http_authproxy_always_onboard"
|
||||
HTTPAuthProxySkipSearch = "http_authproxy_skip_search"
|
||||
OIDCName = "oidc_name"
|
||||
OIDCEndpoint = "oidc_endpoint"
|
||||
OIDCCLientID = "oidc_client_id"
|
||||
@ -120,8 +120,9 @@ const (
|
||||
NotaryURL = "notary_url"
|
||||
DefaultCoreEndpoint = "http://core:8080"
|
||||
DefaultNotaryEndpoint = "http://notary-server:4443"
|
||||
LdapGroupType = 1
|
||||
LdapGroupAdminDn = "ldap_group_admin_dn"
|
||||
LDAPGroupType = 1
|
||||
HTTPGroupType = 2
|
||||
LDAPGroupAdminDn = "ldap_group_admin_dn"
|
||||
LDAPGroupMembershipAttribute = "ldap_group_membership_attribute"
|
||||
DefaultRegistryControllerEndpoint = "http://registryctl:8080"
|
||||
WithChartMuseum = "with_chartmuseum"
|
||||
|
@ -183,6 +183,7 @@ func paginateForQuerySetter(qs orm.QuerySeter, page, size int64) orm.QuerySeter
|
||||
|
||||
// Escape ..
|
||||
func Escape(str string) string {
|
||||
str = strings.Replace(str, `\`, `\\`, -1)
|
||||
str = strings.Replace(str, `%`, `\%`, -1)
|
||||
str = strings.Replace(str, `_`, `\_`, -1)
|
||||
return str
|
||||
|
@ -54,7 +54,7 @@ func GetConfigEntries() ([]*models.ConfigEntry, error) {
|
||||
func SaveConfigEntries(entries []models.ConfigEntry) error {
|
||||
o := GetOrmer()
|
||||
for _, entry := range entries {
|
||||
if entry.Key == common.LdapGroupAdminDn {
|
||||
if entry.Key == common.LDAPGroupAdminDn {
|
||||
entry.Value = utils.TrimLower(entry.Value)
|
||||
}
|
||||
tempEntry := models.ConfigEntry{}
|
||||
|
@ -302,9 +302,6 @@ func TestListUsers(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in ListUsers: %v", err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Errorf("Expect one user in list, but the acutal length is %d, the list: %+v", len(users), users)
|
||||
}
|
||||
users2, err := ListUsers(&models.UserQuery{Username: username})
|
||||
if len(users2) != 1 {
|
||||
t.Errorf("Expect one user in list, but the acutal length is %d, the list: %+v", len(users), users)
|
||||
|
@ -18,23 +18,35 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrGroupNameDup ...
|
||||
var ErrGroupNameDup = errors.New("duplicated user group name")
|
||||
|
||||
// AddUserGroup - Add User Group
|
||||
func AddUserGroup(userGroup models.UserGroup) (int, error) {
|
||||
userGroupList, err := QueryUserGroup(models.UserGroup{GroupName: userGroup.GroupName, GroupType: common.HTTPGroupType})
|
||||
if err != nil {
|
||||
return 0, ErrGroupNameDup
|
||||
}
|
||||
if len(userGroupList) > 0 {
|
||||
return 0, ErrGroupNameDup
|
||||
}
|
||||
o := dao.GetOrmer()
|
||||
|
||||
sql := "insert into user_group (group_name, group_type, ldap_group_dn, creation_time, update_time) values (?, ?, ?, ?, ?) RETURNING id"
|
||||
var id int
|
||||
now := time.Now()
|
||||
|
||||
err := o.Raw(sql, userGroup.GroupName, userGroup.GroupType, utils.TrimLower(userGroup.LdapGroupDN), now, now).QueryRow(&id)
|
||||
err = o.Raw(sql, userGroup.GroupName, userGroup.GroupType, utils.TrimLower(userGroup.LdapGroupDN), now, now).QueryRow(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -47,10 +59,10 @@ func QueryUserGroup(query models.UserGroup) ([]*models.UserGroup, error) {
|
||||
o := dao.GetOrmer()
|
||||
sql := `select id, group_name, group_type, ldap_group_dn from user_group where 1=1 `
|
||||
sqlParam := make([]interface{}, 1)
|
||||
groups := []*models.UserGroup{}
|
||||
var groups []*models.UserGroup
|
||||
if len(query.GroupName) != 0 {
|
||||
sql += ` and group_name like ? `
|
||||
sqlParam = append(sqlParam, `%`+dao.Escape(query.GroupName)+`%`)
|
||||
sql += ` and group_name = ? `
|
||||
sqlParam = append(sqlParam, query.GroupName)
|
||||
}
|
||||
|
||||
if query.GroupType != 0 {
|
||||
@ -86,6 +98,27 @@ func GetUserGroup(id int) (*models.UserGroup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetGroupIDByGroupName - Return the group ID by given group name. it is possible less group ID than the given group name if some group doesn't exist.
|
||||
func GetGroupIDByGroupName(groupName []string, groupType int) ([]int, error) {
|
||||
var retGroupID []int
|
||||
var conditions []string
|
||||
if len(groupName) == 0 {
|
||||
return retGroupID, nil
|
||||
}
|
||||
for _, gName := range groupName {
|
||||
con := "'" + gName + "'"
|
||||
conditions = append(conditions, con)
|
||||
}
|
||||
sql := fmt.Sprintf("select id from user_group where group_name in ( %s ) and group_type = %v", strings.Join(conditions, ","), groupType)
|
||||
o := dao.GetOrmer()
|
||||
cnt, err := o.Raw(sql).QueryRows(&retGroupID)
|
||||
if err != nil {
|
||||
return retGroupID, err
|
||||
}
|
||||
log.Debugf("Found rows %v", cnt)
|
||||
return retGroupID, nil
|
||||
}
|
||||
|
||||
// DeleteUserGroup ...
|
||||
func DeleteUserGroup(id int) error {
|
||||
userGroup := models.UserGroup{ID: id}
|
||||
@ -139,20 +172,3 @@ func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttri
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGroupDNQueryCondition get the part of IN ('XXX', 'XXX') condition
|
||||
func GetGroupDNQueryCondition(userGroupList []*models.UserGroup) string {
|
||||
result := make([]string, 0)
|
||||
count := 0
|
||||
for _, userGroup := range userGroupList {
|
||||
if userGroup.GroupType == common.LdapGroupType {
|
||||
result = append(result, "'"+userGroup.LdapGroupDN+"'")
|
||||
count++
|
||||
}
|
||||
}
|
||||
// No LDAP Group found
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package group
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
@ -46,8 +47,13 @@ func TestMain(m *testing.M) {
|
||||
// Extract to test utils
|
||||
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 ('grouptestu09', 'grouptestu09@example.com', '123456', 'grouptestu09')",
|
||||
"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_http_group', 2, '')",
|
||||
"insert into user_group (group_name, group_type, ldap_group_dn) values ('test_myhttp_group', 2, '')",
|
||||
"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 id from user_group where group_name = 'test_group_01'), 'g', 1)",
|
||||
@ -55,11 +61,14 @@ func TestMain(m *testing.M) {
|
||||
|
||||
clearSqls := []string{
|
||||
"delete from project where name='member_test_01'",
|
||||
"delete from harbor_user where username='member_test_01' or username='pm_sample'",
|
||||
"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' or username='grouptestu09'",
|
||||
"delete from user_group",
|
||||
"delete from project_member",
|
||||
}
|
||||
dao.PrepareTestData(clearSqls, initSqls)
|
||||
dao.ExecuteBatchSQL(initSqls)
|
||||
defer dao.ExecuteBatchSQL(clearSqls)
|
||||
|
||||
result = m.Run()
|
||||
|
||||
@ -80,7 +89,7 @@ func TestAddUserGroup(t *testing.T) {
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Insert an ldap user group", args{userGroup: models.UserGroup{GroupName: "sample_group", GroupType: common.LdapGroupType, LdapGroupDN: "sample_ldap_dn_string"}}, 0, false},
|
||||
{"Insert an ldap user group", args{userGroup: models.UserGroup{GroupName: "sample_group", GroupType: common.LDAPGroupType, LdapGroupDN: "sample_ldap_dn_string"}}, 0, false},
|
||||
{"Insert other user group", args{userGroup: models.UserGroup{GroupName: "other_group", GroupType: 3, LdapGroupDN: "other information"}}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@ -108,8 +117,8 @@ func TestQueryUserGroup(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{"Query all user group", args{query: models.UserGroup{GroupName: "test_group_01"}}, 1, false},
|
||||
{"Query all ldap group", args{query: models.UserGroup{GroupType: common.LdapGroupType}}, 2, false},
|
||||
{"Query ldap group with group property", args{query: models.UserGroup{GroupType: common.LdapGroupType, LdapGroupDN: "CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com"}}, 1, false},
|
||||
{"Query all ldap group", args{query: models.UserGroup{GroupType: common.LDAPGroupType}}, 2, false},
|
||||
{"Query ldap group with group property", args{query: models.UserGroup{GroupType: common.LDAPGroupType, LdapGroupDN: "CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com"}}, 1, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -126,7 +135,7 @@ func TestQueryUserGroup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetUserGroup(t *testing.T) {
|
||||
userGroup := models.UserGroup{GroupName: "insert_group", GroupType: common.LdapGroupType, LdapGroupDN: "ldap_dn_string"}
|
||||
userGroup := models.UserGroup{GroupName: "insert_group", GroupType: common.LDAPGroupType, LdapGroupDN: "ldap_dn_string"}
|
||||
result, err := AddUserGroup(userGroup)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when AddUserGroup: %v", err)
|
||||
@ -175,7 +184,7 @@ func TestUpdateUserGroup(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fmt.Printf("id=%v", createdUserGroupID)
|
||||
fmt.Printf("id=%v\n", createdUserGroupID)
|
||||
if err := UpdateUserGroupName(tt.args.id, tt.args.groupName); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UpdateUserGroup() error = %v, wantErr %v", err, tt.wantErr)
|
||||
userGroup, err := GetUserGroup(tt.args.id)
|
||||
@ -231,13 +240,18 @@ func TestOnBoardUserGroup(t *testing.T) {
|
||||
args{g: &models.UserGroup{
|
||||
GroupName: "harbor_example",
|
||||
LdapGroupDN: "cn=harbor_example,ou=groups,dc=example,dc=com",
|
||||
GroupType: common.LdapGroupType}},
|
||||
GroupType: common.LDAPGroupType}},
|
||||
false},
|
||||
{"OnBoardUserGroup second time",
|
||||
args{g: &models.UserGroup{
|
||||
GroupName: "harbor_example",
|
||||
LdapGroupDN: "cn=harbor_example,ou=groups,dc=example,dc=com",
|
||||
GroupType: common.LdapGroupType}},
|
||||
GroupType: common.LDAPGroupType}},
|
||||
false},
|
||||
{"OnBoardUserGroup HTTP user group",
|
||||
args{g: &models.UserGroup{
|
||||
GroupName: "test_myhttp_group",
|
||||
GroupType: common.HTTPGroupType}},
|
||||
false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@ -249,47 +263,7 @@ func TestOnBoardUserGroup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroupDNQueryCondition(t *testing.T) {
|
||||
userGroupList := []*models.UserGroup{
|
||||
{
|
||||
GroupName: "sample1",
|
||||
GroupType: 1,
|
||||
LdapGroupDN: "cn=sample1_users,ou=groups,dc=example,dc=com",
|
||||
},
|
||||
{
|
||||
GroupName: "sample2",
|
||||
GroupType: 1,
|
||||
LdapGroupDN: "cn=sample2_users,ou=groups,dc=example,dc=com",
|
||||
},
|
||||
{
|
||||
GroupName: "sample3",
|
||||
GroupType: 0,
|
||||
LdapGroupDN: "cn=sample3_users,ou=groups,dc=example,dc=com",
|
||||
},
|
||||
}
|
||||
|
||||
groupQueryConditions := GetGroupDNQueryCondition(userGroupList)
|
||||
expectedConditions := `'cn=sample1_users,ou=groups,dc=example,dc=com','cn=sample2_users,ou=groups,dc=example,dc=com'`
|
||||
if groupQueryConditions != expectedConditions {
|
||||
t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", expectedConditions, groupQueryConditions)
|
||||
}
|
||||
var userGroupList2 []*models.UserGroup
|
||||
groupQueryCondition2 := GetGroupDNQueryCondition(userGroupList2)
|
||||
if len(groupQueryCondition2) > 0 {
|
||||
t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", "", groupQueryCondition2)
|
||||
}
|
||||
groupQueryCondition3 := GetGroupDNQueryCondition(nil)
|
||||
if len(groupQueryCondition3) > 0 {
|
||||
t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", "", groupQueryCondition3)
|
||||
}
|
||||
}
|
||||
func TestGetGroupProjects(t *testing.T) {
|
||||
userID, err := dao.Register(models.User{
|
||||
Username: "grouptestu09",
|
||||
Email: "grouptest09@example.com",
|
||||
Password: "Harbor123456",
|
||||
})
|
||||
defer dao.DeleteUser(int(userID))
|
||||
projectID1, err := dao.AddProject(models.Project{
|
||||
Name: "grouptest01",
|
||||
OwnerID: 1,
|
||||
@ -307,7 +281,7 @@ func TestGetGroupProjects(t *testing.T) {
|
||||
}
|
||||
defer dao.DeleteProject(projectID2)
|
||||
groupID, err := AddUserGroup(models.UserGroup{
|
||||
GroupName: "test_group_01",
|
||||
GroupName: "test_group_03",
|
||||
GroupType: 1,
|
||||
LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
})
|
||||
@ -322,8 +296,7 @@ func TestGetGroupProjects(t *testing.T) {
|
||||
})
|
||||
defer project.DeleteProjectMemberByID(pmid)
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
member := &models.MemberQuery{
|
||||
Name: "grouptestu09",
|
||||
@ -335,19 +308,17 @@ func TestGetGroupProjects(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{"Query with group DN",
|
||||
args{"'cn=harbor_users,ou=groups,dc=example,dc=com'",
|
||||
&models.ProjectQueryParam{
|
||||
Member: member,
|
||||
}},
|
||||
args{&models.ProjectQueryParam{
|
||||
Member: member,
|
||||
}},
|
||||
1, false},
|
||||
{"Query without group DN",
|
||||
args{"",
|
||||
&models.ProjectQueryParam{}},
|
||||
args{&models.ProjectQueryParam{}},
|
||||
1, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := dao.GetGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
got, err := dao.GetGroupProjects([]int{groupID}, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@ -377,7 +348,7 @@ func TestGetTotalGroupProjects(t *testing.T) {
|
||||
}
|
||||
defer dao.DeleteProject(projectID2)
|
||||
groupID, err := AddUserGroup(models.UserGroup{
|
||||
GroupName: "test_group_01",
|
||||
GroupName: "test_group_05",
|
||||
GroupType: 1,
|
||||
LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
})
|
||||
@ -392,8 +363,7 @@ func TestGetTotalGroupProjects(t *testing.T) {
|
||||
})
|
||||
defer project.DeleteProjectMemberByID(pmid)
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -401,18 +371,16 @@ func TestGetTotalGroupProjects(t *testing.T) {
|
||||
wantSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Query with group DN",
|
||||
args{"'cn=harbor_users,ou=groups,dc=example,dc=com'",
|
||||
&models.ProjectQueryParam{}},
|
||||
{"Query with group ID",
|
||||
args{&models.ProjectQueryParam{}},
|
||||
1, false},
|
||||
{"Query without group DN",
|
||||
args{"",
|
||||
&models.ProjectQueryParam{}},
|
||||
{"Query without group ID",
|
||||
args{&models.ProjectQueryParam{}},
|
||||
1, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := dao.GetTotalGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
got, err := dao.GetTotalGroupProjects([]int{groupID}, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@ -423,3 +391,86 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroupIDByGroupName(t *testing.T) {
|
||||
groupList, err := QueryUserGroup(models.UserGroup{GroupName: "test_http_group", GroupType: 2})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(groupList) < 0 {
|
||||
t.Error(err)
|
||||
}
|
||||
groupList2, err := QueryUserGroup(models.UserGroup{GroupName: "test_myhttp_group", GroupType: 2})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(groupList2) < 0 {
|
||||
t.Error(err)
|
||||
}
|
||||
var expectGroupID []int
|
||||
type args struct {
|
||||
groupName []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []int
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty query", args{groupName: []string{}}, expectGroupID, false},
|
||||
{"normal query", args{groupName: []string{"test_http_group", "test_myhttp_group"}}, []int{groupList[0].ID, groupList2[0].ID}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetGroupIDByGroupName(tt.args.groupName, common.HTTPGroupType)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetHTTPGroupIDByGroupName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetHTTPGroupIDByGroupName() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -156,18 +156,19 @@ func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
|
||||
// GetGroupProjects - Get user's all projects, including user is the user member of this project
|
||||
// and the user is in the group which is a group member of this project.
|
||||
func GetGroupProjects(groupDNCondition string, query *models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
func GetGroupProjects(groupIDs []int, query *models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
sql, params := projectQueryConditions(query)
|
||||
sql = `select distinct p.project_id, p.name, p.owner_id,
|
||||
p.creation_time, p.update_time ` + sql
|
||||
if len(groupDNCondition) > 0 {
|
||||
groupIDCondition := JoinNumberConditions(groupIDs)
|
||||
if len(groupIDs) > 0 {
|
||||
sql = fmt.Sprintf(
|
||||
`%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time
|
||||
from project p
|
||||
left join project_member pm on p.project_id = pm.project_id
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' and ug.group_type = 1
|
||||
where ug.ldap_group_dn in ( %s ) order by name`,
|
||||
sql, groupDNCondition)
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
|
||||
where ug.id in ( %s ) order by name`,
|
||||
sql, groupIDCondition)
|
||||
}
|
||||
sqlStr, queryParams := CreatePagination(query, sql, params)
|
||||
log.Debugf("query sql:%v", sql)
|
||||
@ -178,10 +179,11 @@ func GetGroupProjects(groupDNCondition string, query *models.ProjectQueryParam)
|
||||
|
||||
// GetTotalGroupProjects - Get the total count of projects, including user is the member of this project and the
|
||||
// user is in the group, which is the group member of this project.
|
||||
func GetTotalGroupProjects(groupDNCondition string, query *models.ProjectQueryParam) (int, error) {
|
||||
func GetTotalGroupProjects(groupIDs []int, query *models.ProjectQueryParam) (int, error) {
|
||||
var sql string
|
||||
sqlCondition, params := projectQueryConditions(query)
|
||||
if len(groupDNCondition) == 0 {
|
||||
groupIDCondition := JoinNumberConditions(groupIDs)
|
||||
if len(groupIDs) == 0 {
|
||||
sql = `select count(1) ` + sqlCondition
|
||||
} else {
|
||||
sql = fmt.Sprintf(
|
||||
@ -189,9 +191,9 @@ func GetTotalGroupProjects(groupDNCondition string, query *models.ProjectQueryPa
|
||||
from ( select p.project_id %s union select p.project_id
|
||||
from project p
|
||||
left join project_member pm on p.project_id = pm.project_id
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' and ug.group_type = 1
|
||||
where ug.ldap_group_dn in ( %s )) t`,
|
||||
sqlCondition, groupDNCondition)
|
||||
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
|
||||
where ug.id in ( %s )) t`,
|
||||
sqlCondition, groupIDCondition)
|
||||
}
|
||||
log.Debugf("query sql:%v", sql)
|
||||
var count int
|
||||
@ -291,24 +293,24 @@ func DeleteProject(id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRolesByLDAPGroup - Get Project roles of the
|
||||
// specified group DN is a member of current project
|
||||
func GetRolesByLDAPGroup(projectID int64, groupDNCondition string) ([]int, error) {
|
||||
// GetRolesByGroupID - Get Project roles of the
|
||||
// specified group is a member of current project
|
||||
func GetRolesByGroupID(projectID int64, groupIDs []int) ([]int, error) {
|
||||
var roles []int
|
||||
if len(groupDNCondition) == 0 {
|
||||
if len(groupIDs) == 0 {
|
||||
return roles, nil
|
||||
}
|
||||
groupIDCondition := JoinNumberConditions(groupIDs)
|
||||
o := GetOrmer()
|
||||
// Because an LDAP user can be memberof multiple groups,
|
||||
// the role is in descent order (1-admin, 2-developer, 3-guest, 4-master), use min to select the max privilege role.
|
||||
sql := fmt.Sprintf(
|
||||
`select min(pm.role) from project_member pm
|
||||
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id
|
||||
where ug.ldap_group_dn in ( %s ) and pm.project_id = ? `,
|
||||
groupDNCondition)
|
||||
where ug.id in ( %s ) and pm.project_id = ?`,
|
||||
groupIDCondition)
|
||||
log.Debugf("sql:%v", sql)
|
||||
if _, err := o.Raw(sql, projectID).QueryRows(&roles); err != nil {
|
||||
log.Warningf("Error in GetRolesByLDAPGroup, error: %v", err)
|
||||
log.Warningf("Error in GetRolesByGroupID, error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// If there is no row selected, the min returns an empty row, to avoid return 0 as role
|
||||
|
@ -148,16 +148,3 @@ func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, e
|
||||
_, err := o.Raw(sql, queryParam).QueryRows(&members)
|
||||
return members, err
|
||||
}
|
||||
|
||||
// GetRolesByGroup -- Query group roles
|
||||
func GetRolesByGroup(projectID int64, groupDNCondition string) []int {
|
||||
var roles []int
|
||||
o := dao.GetOrmer()
|
||||
sql := `select role from project_member pm
|
||||
left join user_group ug on pm.project_id = ?
|
||||
where ug.group_type = 1 and ug.ldap_group_dn in (` + groupDNCondition + `)`
|
||||
if _, err := o.Raw(sql, projectID).QueryRows(&roles); err != nil {
|
||||
return roles
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
@ -305,30 +305,3 @@ func PrepareGroupTest() {
|
||||
}
|
||||
dao.PrepareTestData(clearSqls, initSqls)
|
||||
}
|
||||
func TestGetRolesByGroup(t *testing.T) {
|
||||
PrepareGroupTest()
|
||||
|
||||
project, err := dao.GetProjectByName("group_project")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when GetProjectByName : %v", err)
|
||||
}
|
||||
type args struct {
|
||||
projectID int64
|
||||
groupDNCondition string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []int
|
||||
}{
|
||||
{"Query group with role", args{project.ProjectID, "'cn=harbor_user,dc=example,dc=com'"}, []int{2}},
|
||||
{"Query group no role", args{project.ProjectID, "'cn=another_user,dc=example,dc=com'"}, []int{}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetRolesByGroup(tt.args.projectID, tt.args.groupDNCondition); !dao.ArrayEqual(got, tt.want) {
|
||||
t.Errorf("GetRolesByGroup() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -118,124 +118,6 @@ func Test_projectQueryConditions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroupProjects(t *testing.T) {
|
||||
prepareGroupTest()
|
||||
query := &models.ProjectQueryParam{Member: &models.MemberQuery{Name: "sample_group"}}
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Verify correct sql", args{groupDNCondition: "'cn=harbor_user,dc=example,dc=com'", query: query}, 1, false},
|
||||
{"Verify missed sql", args{groupDNCondition: "'cn=another_user,dc=example,dc=com'", query: query}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if len(got) != tt.wantSize {
|
||||
t.Errorf("GetGroupProjects() = %v, want %v", got, tt.wantSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func prepareGroupTest() {
|
||||
initSqls := []string{
|
||||
`insert into user_group (group_name, group_type, ldap_group_dn) values ('harbor_group_01', 1, 'cn=harbor_user,dc=example,dc=com')`,
|
||||
`insert into harbor_user (username, email, password, realname) values ('sample01', 'sample01@example.com', 'harbor12345', 'sample01')`,
|
||||
`insert into project (name, owner_id) values ('group_project', 1)`,
|
||||
`insert into project (name, owner_id) values ('group_project_private', 1)`,
|
||||
`insert into project_metadata (project_id, name, value) values ((select project_id from project where name = 'group_project'), 'public', 'false')`,
|
||||
`insert into project_metadata (project_id, name, value) values ((select project_id from project where name = 'group_project_private'), 'public', 'false')`,
|
||||
`insert into project_member (project_id, entity_id, entity_type, role) values ((select project_id from project where name = 'group_project'), (select id from user_group where group_name = 'harbor_group_01'),'g', 2)`,
|
||||
}
|
||||
|
||||
clearSqls := []string{
|
||||
`delete from project_metadata where project_id in (select project_id from project where name in ('group_project', 'group_project_private'))`,
|
||||
`delete from project where name in ('group_project', 'group_project_private')`,
|
||||
`delete from project_member where project_id in (select project_id from project where name in ('group_project', 'group_project_private'))`,
|
||||
`delete from user_group where group_name = 'harbor_group_01'`,
|
||||
`delete from harbor_user where username = 'sample01'`,
|
||||
}
|
||||
PrepareTestData(clearSqls, initSqls)
|
||||
}
|
||||
|
||||
func TestGetTotalGroupProjects(t *testing.T) {
|
||||
prepareGroupTest()
|
||||
query := &models.ProjectQueryParam{Member: &models.MemberQuery{Name: "sample_group"}}
|
||||
type args struct {
|
||||
groupDNCondition string
|
||||
query *models.ProjectQueryParam
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Verify correct sql", args{groupDNCondition: "'cn=harbor_user,dc=example,dc=com'", query: query}, 1, false},
|
||||
{"Verify missed sql", args{groupDNCondition: "'cn=another_user,dc=example,dc=com'", query: query}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetTotalGroupProjects(tt.args.groupDNCondition, tt.args.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTotalGroupProjects() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GetTotalGroupProjects() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRolesByLDAPGroup(t *testing.T) {
|
||||
prepareGroupTest()
|
||||
project, err := GetProjectByName("group_project")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when Get project by name: %v", err)
|
||||
}
|
||||
privateProject, err := GetProjectByName("group_project_private")
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when Get project by name: %v", err)
|
||||
}
|
||||
type args struct {
|
||||
projectID int64
|
||||
groupDNCondition string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"Check normal", args{project.ProjectID, "'cn=harbor_user,dc=example,dc=com'"}, 1, false},
|
||||
{"Check non exist", args{privateProject.ProjectID, "'cn=not_harbor_user,dc=example,dc=com'"}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetRolesByLDAPGroup(tt.args.projectID, tt.args.groupDNCondition)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TestGetRolesByLDAPGroup() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if len(got) != tt.wantSize {
|
||||
t.Errorf("TestGetRolesByLDAPGroup() = %v, want %v", len(got), tt.wantSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjetExistsByName(t *testing.T) {
|
||||
name := "project_exist_by_name_test"
|
||||
exist := ProjectExistsByName(name)
|
||||
|
@ -120,6 +120,19 @@ func PrepareTestData(clearSqls []string, initSqls []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteBatchSQL ...
|
||||
func ExecuteBatchSQL(sqls []string) {
|
||||
o := GetOrmer()
|
||||
|
||||
for _, sql := range sqls {
|
||||
fmt.Printf("Exec sql:%v\n", sql)
|
||||
_, err := o.Raw(sql).Exec()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to execute batch sql, sql:%v, error: %v", sql, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayEqual ...
|
||||
func ArrayEqual(arrayA, arrayB []int) bool {
|
||||
if len(arrayA) != len(arrayB) {
|
||||
|
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -70,7 +70,7 @@ type HTTPAuthProxy struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
TokenReviewEndpoint string `json:"tokenreivew_endpoint"`
|
||||
VerifyCert bool `json:"verify_cert"`
|
||||
AlwaysOnBoard bool `json:"always_onboard"`
|
||||
SkipSearch bool `json:"skip_search"`
|
||||
}
|
||||
|
||||
// OIDCSetting wraps the settings for OIDC auth endpoint
|
||||
|
@ -20,16 +20,17 @@ import (
|
||||
|
||||
// keys of project metadata and severity values
|
||||
const (
|
||||
ProMetaPublic = "public"
|
||||
ProMetaEnableContentTrust = "enable_content_trust"
|
||||
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
|
||||
ProMetaSeverity = "severity"
|
||||
ProMetaAutoScan = "auto_scan"
|
||||
SeverityNone = "negligible"
|
||||
SeverityLow = "low"
|
||||
SeverityMedium = "medium"
|
||||
SeverityHigh = "high"
|
||||
SeverityCritical = "critical"
|
||||
ProMetaPublic = "public"
|
||||
ProMetaEnableContentTrust = "enable_content_trust"
|
||||
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
|
||||
ProMetaSeverity = "severity"
|
||||
ProMetaAutoScan = "auto_scan"
|
||||
ProMetaReuseSysCVEWhitelist = "reuse_sys_cve_whitelist"
|
||||
SeverityNone = "negligible"
|
||||
SeverityLow = "low"
|
||||
SeverityMedium = "medium"
|
||||
SeverityHigh = "high"
|
||||
SeverityCritical = "critical"
|
||||
)
|
||||
|
||||
// ProjectMetadata holds the metadata of a project.
|
||||
|
@ -36,6 +36,7 @@ type Project struct {
|
||||
RepoCount int64 `orm:"-" json:"repo_count"`
|
||||
ChartCount uint64 `orm:"-" json:"chart_count"`
|
||||
Metadata map[string]string `orm:"-" json:"metadata"`
|
||||
CVEWhitelist CVEWhitelist `orm:"-" json:"cve_whitelist"`
|
||||
}
|
||||
|
||||
// GetMetadata ...
|
||||
@ -83,6 +84,15 @@ func (p *Project) VulPrevented() bool {
|
||||
return isTrue(prevent)
|
||||
}
|
||||
|
||||
// ReuseSysCVEWhitelist ...
|
||||
func (p *Project) ReuseSysCVEWhitelist() bool {
|
||||
r, ok := p.GetMetadata(ProMetaReuseSysCVEWhitelist)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return isTrue(r)
|
||||
}
|
||||
|
||||
// Severity ...
|
||||
func (p *Project) Severity() string {
|
||||
severity, exist := p.GetMetadata(ProMetaSeverity)
|
||||
@ -128,9 +138,9 @@ type ProjectQueryParam struct {
|
||||
|
||||
// MemberQuery filter by member's username and role
|
||||
type MemberQuery struct {
|
||||
Name string // the username of member
|
||||
Role int // the role of the member has to the project
|
||||
GroupList []*UserGroup // the group list of current user
|
||||
Name string // the username of member
|
||||
Role int // the role of the member has to the project
|
||||
GroupIDs []int // the group ID of current user belongs to
|
||||
}
|
||||
|
||||
// Pagination ...
|
||||
@ -154,9 +164,10 @@ type BaseProjectCollection struct {
|
||||
|
||||
// ProjectRequest holds informations that need for creating project API
|
||||
type ProjectRequest struct {
|
||||
Name string `json:"project_name"`
|
||||
Public *int `json:"public"` // deprecated, reserved for project creation in replication
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Name string `json:"project_name"`
|
||||
Public *int `json:"public"` // deprecated, reserved for project creation in replication
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
CVEWhitelist CVEWhitelist `json:"cve_whitelist"`
|
||||
}
|
||||
|
||||
// ProjectQueryResult ...
|
||||
|
@ -35,13 +35,13 @@ type User struct {
|
||||
// to it.
|
||||
Role int `orm:"-" json:"role_id"`
|
||||
// RoleList []Role `json:"role_list"`
|
||||
HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"`
|
||||
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
|
||||
Salt string `orm:"column(salt)" json:"-"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
GroupList []*UserGroup `orm:"-" json:"-"`
|
||||
OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta,omitempty"`
|
||||
HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"`
|
||||
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
|
||||
Salt string `orm:"column(salt)" json:"-"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
GroupIDs []int `orm:"-" json:"-"`
|
||||
OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta,omitempty"`
|
||||
}
|
||||
|
||||
// UserQuery ...
|
||||
|
@ -17,7 +17,6 @@ package local
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
@ -140,12 +139,11 @@ func (s *SecurityContext) GetRolesByGroup(projectIDOrName interface{}) []int {
|
||||
user := s.user
|
||||
project, err := s.pm.Get(projectIDOrName)
|
||||
// No user, group or project info
|
||||
if err != nil || project == nil || user == nil || len(user.GroupList) == 0 {
|
||||
if err != nil || project == nil || user == nil || len(user.GroupIDs) == 0 {
|
||||
return roles
|
||||
}
|
||||
// Get role by LDAP group
|
||||
groupDNConditions := group.GetGroupDNQueryCondition(user.GroupList)
|
||||
roles, err = dao.GetRolesByLDAPGroup(project.ProjectID, groupDNConditions)
|
||||
// Get role by Group ID
|
||||
roles, err = dao.GetRolesByGroupID(project.ProjectID, user.GroupIDs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -157,8 +155,8 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
|
||||
result, err := s.pm.List(
|
||||
&models.ProjectQueryParam{
|
||||
Member: &models.MemberQuery{
|
||||
Name: s.GetUsername(),
|
||||
GroupList: s.user.GroupList,
|
||||
Name: s.GetUsername(),
|
||||
GroupIDs: s.user.GroupIDs,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
@ -253,9 +254,16 @@ func TestHasPushPullPermWithGroup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when GetUser: %v", err)
|
||||
}
|
||||
developer.GroupList = []*models.UserGroup{
|
||||
{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"},
|
||||
|
||||
userGroups, err := group.QueryUserGroup(models.UserGroup{GroupType: common.LDAPGroupType, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to query user group %v", err)
|
||||
}
|
||||
if len(userGroups) < 1 {
|
||||
t.Errorf("Failed to retrieve user group")
|
||||
}
|
||||
|
||||
developer.GroupIDs = []int{userGroups[0].ID}
|
||||
|
||||
resource := rbac.NewProjectNamespace(project.Name).Resource(rbac.ResourceRepository)
|
||||
|
||||
@ -332,9 +340,15 @@ func TestSecurityContext_GetRolesByGroup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when GetUser: %v", err)
|
||||
}
|
||||
developer.GroupList = []*models.UserGroup{
|
||||
{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"},
|
||||
userGroups, err := group.QueryUserGroup(models.UserGroup{GroupType: common.LDAPGroupType, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to query user group %v", err)
|
||||
}
|
||||
if len(userGroups) < 1 {
|
||||
t.Errorf("Failed to retrieve user group")
|
||||
}
|
||||
|
||||
developer.GroupIDs = []int{userGroups[0].ID}
|
||||
type fields struct {
|
||||
user *models.User
|
||||
pm promgr.ProjectManager
|
||||
|
@ -35,20 +35,14 @@ const googleEndpoint = "https://accounts.google.com"
|
||||
|
||||
type providerHelper struct {
|
||||
sync.Mutex
|
||||
ep endpoint
|
||||
instance atomic.Value
|
||||
setting atomic.Value
|
||||
}
|
||||
|
||||
type endpoint struct {
|
||||
url string
|
||||
VerifyCert bool
|
||||
instance atomic.Value
|
||||
setting atomic.Value
|
||||
creationTime time.Time
|
||||
}
|
||||
|
||||
func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
if p.instance.Load() != nil {
|
||||
s := p.setting.Load().(models.OIDCSetting)
|
||||
if s.Endpoint != p.ep.url || s.VerifyCert != p.ep.VerifyCert { // relevant settings have changed, need to re-create provider.
|
||||
if time.Now().Sub(p.creationTime) > 3*time.Second {
|
||||
if err := p.create(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -57,7 +51,7 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
if p.instance.Load() == nil {
|
||||
if err := p.reload(); err != nil {
|
||||
if err := p.reloadSetting(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.create(); err != nil {
|
||||
@ -65,7 +59,7 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
if err := p.reload(); err != nil {
|
||||
if err := p.reloadSetting(); err != nil {
|
||||
log.Warningf("Failed to refresh configuration, error: %v", err)
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
@ -73,10 +67,11 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return p.instance.Load().(*gooidc.Provider), nil
|
||||
}
|
||||
|
||||
func (p *providerHelper) reload() error {
|
||||
func (p *providerHelper) reloadSetting() error {
|
||||
conf, err := config.OIDCSetting()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load OIDC setting: %v", err)
|
||||
@ -96,10 +91,7 @@ func (p *providerHelper) create() error {
|
||||
return fmt.Errorf("failed to create OIDC provider, error: %v", err)
|
||||
}
|
||||
p.instance.Store(provider)
|
||||
p.ep = endpoint{
|
||||
url: s.Endpoint,
|
||||
VerifyCert: s.VerifyCert,
|
||||
}
|
||||
p.creationTime = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -49,21 +49,20 @@ func TestMain(m *testing.M) {
|
||||
func TestHelperLoadConf(t *testing.T) {
|
||||
testP := &providerHelper{}
|
||||
assert.Nil(t, testP.setting.Load())
|
||||
err := testP.reload()
|
||||
err := testP.reloadSetting()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test", testP.setting.Load().(models.OIDCSetting).Name)
|
||||
assert.Equal(t, endpoint{}, testP.ep)
|
||||
}
|
||||
|
||||
func TestHelperCreate(t *testing.T) {
|
||||
testP := &providerHelper{}
|
||||
err := testP.reload()
|
||||
err := testP.reloadSetting()
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, testP.instance.Load())
|
||||
err = testP.create()
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, "https://accounts.google.com", testP.ep.url)
|
||||
assert.NotNil(t, testP.instance.Load())
|
||||
assert.True(t, time.Now().Sub(testP.creationTime) < 2*time.Second)
|
||||
}
|
||||
|
||||
func TestHelperGet(t *testing.T) {
|
||||
|
114
src/common/utils/redis/helper.go
Normal file
114
src/common/utils/redis/helper.go
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnLock ...
|
||||
ErrUnLock = errors.New("error to release the redis lock")
|
||||
)
|
||||
|
||||
const (
|
||||
unlockScript = `
|
||||
if redis.call("get",KEYS[1]) == ARGV[1] then
|
||||
return redis.call("del",KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`
|
||||
defaultDelay = 5 * time.Second
|
||||
defaultMaxRetry = 5
|
||||
defaultExpiry = 600 * time.Second
|
||||
)
|
||||
|
||||
// Mutex ...
|
||||
type Mutex struct {
|
||||
Conn redis.Conn
|
||||
key string
|
||||
value string
|
||||
opts Options
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(conn redis.Conn, key, value string) *Mutex {
|
||||
o := *DefaultOptions()
|
||||
if value == "" {
|
||||
value = utils.GenerateRandomString()
|
||||
}
|
||||
return &Mutex{conn, key, value, o}
|
||||
}
|
||||
|
||||
// Require retry to require the lock
|
||||
func (rm *Mutex) Require() (bool, error) {
|
||||
var isRequired bool
|
||||
var err error
|
||||
|
||||
for i := 0; i < rm.opts.maxRetry; i++ {
|
||||
isRequired, err = rm.require()
|
||||
if isRequired {
|
||||
break
|
||||
}
|
||||
if err != nil || !isRequired {
|
||||
time.Sleep(rm.opts.retryDelay)
|
||||
}
|
||||
}
|
||||
|
||||
return isRequired, err
|
||||
}
|
||||
|
||||
// require get the redis lock, for details, just refer to https://redis.io/topics/distlock
|
||||
func (rm *Mutex) require() (bool, error) {
|
||||
reply, err := redis.String(rm.Conn.Do("SET", rm.key, rm.value, "NX", "PX", int(rm.opts.expiry/time.Millisecond)))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return reply == "OK", nil
|
||||
}
|
||||
|
||||
// Free releases the lock, for details, just refer to https://redis.io/topics/distlock
|
||||
func (rm *Mutex) Free() (bool, error) {
|
||||
script := redis.NewScript(1, unlockScript)
|
||||
resp, err := redis.Int(script.Do(rm.Conn, rm.key, rm.value))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp == 0 {
|
||||
return false, ErrUnLock
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Options ...
|
||||
type Options struct {
|
||||
retryDelay time.Duration
|
||||
expiry time.Duration
|
||||
maxRetry int
|
||||
}
|
||||
|
||||
// DefaultOptions ...
|
||||
func DefaultOptions() *Options {
|
||||
opt := &Options{
|
||||
retryDelay: defaultDelay,
|
||||
expiry: defaultExpiry,
|
||||
maxRetry: defaultMaxRetry,
|
||||
}
|
||||
return opt
|
||||
}
|
62
src/common/utils/redis/helper_test.go
Normal file
62
src/common/utils/redis/helper_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testingRedisHost = "REDIS_HOST"
|
||||
|
||||
func TestRedisLock(t *testing.T) {
|
||||
con, err := redis.Dial(
|
||||
"tcp",
|
||||
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||
redis.DialConnectTimeout(30*time.Second),
|
||||
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||
redis.DialWriteTimeout(10*time.Second),
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
defer con.Close()
|
||||
|
||||
rm := New(con, "test-redis-lock", "test-value")
|
||||
|
||||
successLock, err := rm.Require()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, successLock)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
_, err = rm.Require()
|
||||
assert.NotNil(t, err)
|
||||
|
||||
successUnLock, err := rm.Free()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, successUnLock)
|
||||
|
||||
}
|
||||
|
||||
func getRedisHost() string {
|
||||
redisHost := os.Getenv(testingRedisHost)
|
||||
if redisHost == "" {
|
||||
redisHost = "127.0.0.1" // for local test
|
||||
}
|
||||
|
||||
return redisHost
|
||||
}
|
@ -22,10 +22,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/gorilla/mux"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// RequestHandlerMapping is a mapping between request and its handler
|
||||
@ -120,7 +121,7 @@ func GetUnitTestConfig() map[string]interface{} {
|
||||
common.LDAPGroupBaseDN: "dc=example,dc=com",
|
||||
common.LDAPGroupAttributeName: "cn",
|
||||
common.LDAPGroupSearchScope: 2,
|
||||
common.LdapGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
common.LDAPGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
common.WithNotary: "false",
|
||||
common.WithChartMuseum: "false",
|
||||
common.SelfRegistration: "true",
|
||||
|
@ -207,6 +207,17 @@ func TestMain(m *testing.M) {
|
||||
if err := prepare(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dao.ExecuteBatchSQL([]string{
|
||||
"insert into user_group (group_name, group_type, ldap_group_dn) values ('test_group_01_api', 1, 'cn=harbor_users,ou=sample,ou=vmware,dc=harbor,dc=com')",
|
||||
"insert into user_group (group_name, group_type, ldap_group_dn) values ('vsphere.local\\administrators', 2, '')",
|
||||
})
|
||||
|
||||
defer dao.ExecuteBatchSQL([]string{
|
||||
"delete from harbor_label",
|
||||
"delete from robot",
|
||||
"delete from user_group",
|
||||
"delete from project_member",
|
||||
})
|
||||
|
||||
ret := m.Run()
|
||||
clean()
|
||||
|
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !darwin
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
|
@ -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)
|
||||
|
@ -23,11 +23,13 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
// ProjectMemberAPI handles request to /api/projects/{}/members/{}
|
||||
@ -37,6 +39,7 @@ type ProjectMemberAPI struct {
|
||||
entityID int
|
||||
entityType string
|
||||
project *models.Project
|
||||
groupType int
|
||||
}
|
||||
|
||||
// ErrDuplicateProjectMember ...
|
||||
@ -84,6 +87,15 @@ func (pma *ProjectMemberAPI) Prepare() {
|
||||
return
|
||||
}
|
||||
pma.id = int(pmid)
|
||||
authMode, err := config.AuthMode()
|
||||
if err != nil {
|
||||
pma.SendInternalServerError(fmt.Errorf("failed to get authentication mode"))
|
||||
}
|
||||
if authMode == common.LDAPAuth {
|
||||
pma.groupType = common.LDAPGroupType
|
||||
} else if authMode == common.HTTPAuth {
|
||||
pma.groupType = common.HTTPGroupType
|
||||
}
|
||||
}
|
||||
|
||||
func (pma *ProjectMemberAPI) requireAccess(action rbac.Action) bool {
|
||||
@ -131,7 +143,7 @@ func (pma *ProjectMemberAPI) Get() {
|
||||
return
|
||||
}
|
||||
if len(memberList) == 0 {
|
||||
pma.SendNotFoundError(fmt.Errorf("The project member does not exit, pmid:%v", pma.id))
|
||||
pma.SendNotFoundError(fmt.Errorf("The project member does not exist, pmid:%v", pma.id))
|
||||
return
|
||||
}
|
||||
|
||||
@ -161,10 +173,10 @@ func (pma *ProjectMemberAPI) Post() {
|
||||
pma.SendBadRequestError(fmt.Errorf("Failed to add project member, error: %v", err))
|
||||
return
|
||||
} else if err == auth.ErrDuplicateLDAPGroup {
|
||||
pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist LDAP group or project member, groupDN:%v", request.MemberGroup.LdapGroupDN))
|
||||
pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist group or project member, groupDN:%v", request.MemberGroup.LdapGroupDN))
|
||||
return
|
||||
} else if err == ErrDuplicateProjectMember {
|
||||
pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist LDAP group or project member, groupMemberID:%v", request.MemberGroup.ID))
|
||||
pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist group or project member, groupMemberID:%v", request.MemberGroup.ID))
|
||||
return
|
||||
} else if err == ErrInvalidRole {
|
||||
pma.SendBadRequestError(fmt.Errorf("Invalid role ID, role ID %v", request.Role))
|
||||
@ -220,12 +232,13 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) {
|
||||
var member models.Member
|
||||
member.ProjectID = projectID
|
||||
member.Role = request.Role
|
||||
member.EntityType = common.GroupMember
|
||||
|
||||
if request.MemberUser.UserID > 0 {
|
||||
member.EntityID = request.MemberUser.UserID
|
||||
member.EntityType = common.UserMember
|
||||
} else if request.MemberGroup.ID > 0 {
|
||||
member.EntityID = request.MemberGroup.ID
|
||||
member.EntityType = common.GroupMember
|
||||
} else if len(request.MemberUser.Username) > 0 {
|
||||
var userID int
|
||||
member.EntityType = common.UserMember
|
||||
@ -243,14 +256,28 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) {
|
||||
}
|
||||
member.EntityID = userID
|
||||
} else if len(request.MemberGroup.LdapGroupDN) > 0 {
|
||||
|
||||
request.MemberGroup.GroupType = common.LDAPGroupType
|
||||
// If groupname provided, use the provided groupname to name this group
|
||||
groupID, err := auth.SearchAndOnBoardGroup(request.MemberGroup.LdapGroupDN, request.MemberGroup.GroupName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
member.EntityID = groupID
|
||||
member.EntityType = common.GroupMember
|
||||
} else if len(request.MemberGroup.GroupName) > 0 && request.MemberGroup.GroupType == common.HTTPGroupType {
|
||||
ugs, err := group.QueryUserGroup(models.UserGroup{GroupName: request.MemberGroup.GroupName, GroupType: common.HTTPGroupType})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(ugs) == 0 {
|
||||
groupID, err := auth.SearchAndOnBoardGroup(request.MemberGroup.GroupName, "")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
member.EntityID = groupID
|
||||
} else {
|
||||
member.EntityID = ugs[0].ID
|
||||
}
|
||||
|
||||
}
|
||||
if member.EntityID <= 0 {
|
||||
return 0, fmt.Errorf("Can not get valid member entity, request: %+v", request)
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
)
|
||||
@ -94,6 +95,21 @@ func TestProjectMemberAPI_Post(t *testing.T) {
|
||||
t.Errorf("Error occurred when create user: %v", err)
|
||||
}
|
||||
|
||||
ugList, err := group.QueryUserGroup(models.UserGroup{GroupType: 1, LdapGroupDN: "cn=harbor_users,ou=sample,ou=vmware,dc=harbor,dc=com"})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to query the user group")
|
||||
}
|
||||
if len(ugList) <= 0 {
|
||||
t.Errorf("Failed to query the user group")
|
||||
}
|
||||
httpUgList, err := group.QueryUserGroup(models.UserGroup{GroupType: 2, GroupName: "vsphere.local\\administrators"})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to query the user group")
|
||||
}
|
||||
if len(httpUgList) <= 0 {
|
||||
t.Errorf("Failed to query the user group")
|
||||
}
|
||||
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
@ -167,6 +183,66 @@ func TestProjectMemberAPI_Post(t *testing.T) {
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/projects/1/members",
|
||||
credential: admin,
|
||||
bodyJSON: &models.MemberReq{
|
||||
Role: 1,
|
||||
MemberGroup: models.UserGroup{
|
||||
GroupType: 1,
|
||||
LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/projects/1/members",
|
||||
credential: admin,
|
||||
bodyJSON: &models.MemberReq{
|
||||
Role: 1,
|
||||
MemberGroup: models.UserGroup{
|
||||
GroupType: 2,
|
||||
ID: httpUgList[0].ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/projects/1/members",
|
||||
credential: admin,
|
||||
bodyJSON: &models.MemberReq{
|
||||
Role: 1,
|
||||
MemberGroup: models.UserGroup{
|
||||
GroupType: 1,
|
||||
ID: ugList[0].ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/projects/1/members",
|
||||
credential: admin,
|
||||
bodyJSON: &models.MemberReq{
|
||||
Role: 1,
|
||||
MemberGroup: models.UserGroup{
|
||||
GroupType: 2,
|
||||
GroupName: "vsphere.local/users",
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -27,12 +27,14 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils/ldap"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
// UserGroupAPI ...
|
||||
type UserGroupAPI struct {
|
||||
BaseController
|
||||
id int
|
||||
id int
|
||||
groupType int
|
||||
}
|
||||
|
||||
const (
|
||||
@ -61,6 +63,15 @@ func (uga *UserGroupAPI) Prepare() {
|
||||
uga.SendForbiddenError(errors.New(uga.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
authMode, err := config.AuthMode()
|
||||
if err != nil {
|
||||
uga.SendInternalServerError(errors.New("failed to get authentication mode"))
|
||||
}
|
||||
if authMode == common.LDAPAuth {
|
||||
uga.groupType = common.LDAPGroupType
|
||||
} else if authMode == common.HTTPAuth {
|
||||
uga.groupType = common.HTTPGroupType
|
||||
}
|
||||
}
|
||||
|
||||
// Get ...
|
||||
@ -69,7 +80,7 @@ func (uga *UserGroupAPI) Get() {
|
||||
uga.Data["json"] = make([]models.UserGroup, 0)
|
||||
if ID == 0 {
|
||||
// user group id not set, return all user group
|
||||
query := models.UserGroup{GroupType: common.LdapGroupType} // Current query LDAP group only
|
||||
query := models.UserGroup{GroupType: uga.groupType}
|
||||
userGroupList, err := group.QueryUserGroup(query)
|
||||
if err != nil {
|
||||
uga.SendInternalServerError(fmt.Errorf("failed to query database for user group list, error: %v", err))
|
||||
@ -103,41 +114,50 @@ func (uga *UserGroupAPI) Post() {
|
||||
}
|
||||
|
||||
userGroup.ID = 0
|
||||
userGroup.GroupType = common.LdapGroupType
|
||||
if userGroup.GroupType == 0 {
|
||||
userGroup.GroupType = uga.groupType
|
||||
}
|
||||
userGroup.LdapGroupDN = strings.TrimSpace(userGroup.LdapGroupDN)
|
||||
userGroup.GroupName = strings.TrimSpace(userGroup.GroupName)
|
||||
if len(userGroup.GroupName) == 0 {
|
||||
uga.SendBadRequestError(errors.New(userNameEmptyMsg))
|
||||
return
|
||||
}
|
||||
query := models.UserGroup{GroupType: userGroup.GroupType, LdapGroupDN: userGroup.LdapGroupDN}
|
||||
result, err := group.QueryUserGroup(query)
|
||||
if err != nil {
|
||||
uga.SendInternalServerError(fmt.Errorf("error occurred in add user group, error: %v", err))
|
||||
return
|
||||
}
|
||||
if len(result) > 0 {
|
||||
uga.SendConflictError(errors.New("error occurred in add user group, duplicate user group exist"))
|
||||
return
|
||||
}
|
||||
// User can not add ldap group when the ldap server is offline
|
||||
ldapGroup, err := auth.SearchGroup(userGroup.LdapGroupDN)
|
||||
if err == ldap.ErrNotFound || ldapGroup == nil {
|
||||
uga.SendBadRequestError(fmt.Errorf("LDAP Group DN is not found: DN:%v", userGroup.LdapGroupDN))
|
||||
return
|
||||
}
|
||||
if err == ldap.ErrDNSyntax {
|
||||
uga.SendBadRequestError(fmt.Errorf("invalid DN syntax. DN: %v", userGroup.LdapGroupDN))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
uga.SendInternalServerError(fmt.Errorf("Error occurred in search user group. error: %v", err))
|
||||
return
|
||||
|
||||
if userGroup.GroupType == common.LDAPGroupType {
|
||||
query := models.UserGroup{GroupType: userGroup.GroupType, LdapGroupDN: userGroup.LdapGroupDN}
|
||||
result, err := group.QueryUserGroup(query)
|
||||
if err != nil {
|
||||
uga.SendInternalServerError(fmt.Errorf("error occurred in add user group, error: %v", err))
|
||||
return
|
||||
}
|
||||
if len(result) > 0 {
|
||||
uga.SendConflictError(errors.New("error occurred in add user group, duplicate user group exist"))
|
||||
return
|
||||
}
|
||||
// User can not add ldap group when the ldap server is offline
|
||||
ldapGroup, err := auth.SearchGroup(userGroup.LdapGroupDN)
|
||||
if err == ldap.ErrNotFound || ldapGroup == nil {
|
||||
uga.SendBadRequestError(fmt.Errorf("LDAP Group DN is not found: DN:%v", userGroup.LdapGroupDN))
|
||||
return
|
||||
}
|
||||
if err == ldap.ErrDNSyntax {
|
||||
uga.SendBadRequestError(fmt.Errorf("invalid DN syntax. DN: %v", userGroup.LdapGroupDN))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
uga.SendInternalServerError(fmt.Errorf("error occurred in search user group. error: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
groupID, err := group.AddUserGroup(userGroup)
|
||||
if err != nil {
|
||||
uga.SendInternalServerError(fmt.Errorf("Error occurred in add user group, error: %v", err))
|
||||
if err == group.ErrGroupNameDup {
|
||||
uga.SendConflictError(fmt.Errorf("duplicated user group name %s", userGroup.GroupName))
|
||||
return
|
||||
}
|
||||
uga.SendInternalServerError(fmt.Errorf("error occurred in add user group, error: %v", err))
|
||||
return
|
||||
}
|
||||
uga.Redirect(http.StatusCreated, strconv.FormatInt(int64(groupID), 10))
|
||||
@ -150,13 +170,17 @@ func (uga *UserGroupAPI) Put() {
|
||||
uga.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
if userGroup.GroupType == common.HTTPGroupType {
|
||||
uga.SendBadRequestError(errors.New("HTTP group is not allowed to update"))
|
||||
return
|
||||
}
|
||||
ID := uga.id
|
||||
userGroup.GroupName = strings.TrimSpace(userGroup.GroupName)
|
||||
if len(userGroup.GroupName) == 0 {
|
||||
uga.SendBadRequestError(errors.New(userNameEmptyMsg))
|
||||
return
|
||||
}
|
||||
userGroup.GroupType = common.LdapGroupType
|
||||
userGroup.GroupType = common.LDAPGroupType
|
||||
log.Debugf("Updated user group %v", userGroup)
|
||||
err := group.UpdateUserGroupName(ID, userGroup.GroupName)
|
||||
if err != nil {
|
||||
|
@ -35,7 +35,7 @@ func TestUserGroupAPI_GetAndDelete(t *testing.T) {
|
||||
groupID, err := group.AddUserGroup(models.UserGroup{
|
||||
GroupName: "harbor_users",
|
||||
LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
GroupType: common.LdapGroupType,
|
||||
GroupType: common.LDAPGroupType,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -88,7 +88,7 @@ func TestUserGroupAPI_Post(t *testing.T) {
|
||||
groupID, err := group.AddUserGroup(models.UserGroup{
|
||||
GroupName: "harbor_group",
|
||||
LdapGroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com",
|
||||
GroupType: common.LdapGroupType,
|
||||
GroupType: common.LDAPGroupType,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred when AddUserGroup: %v", err)
|
||||
@ -104,7 +104,32 @@ func TestUserGroupAPI_Post(t *testing.T) {
|
||||
bodyJSON: &models.UserGroup{
|
||||
GroupName: "harbor_group",
|
||||
LdapGroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com",
|
||||
GroupType: common.LdapGroupType,
|
||||
GroupType: common.LDAPGroupType,
|
||||
},
|
||||
credential: admin,
|
||||
},
|
||||
code: http.StatusConflict,
|
||||
},
|
||||
// 201
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/usergroups",
|
||||
bodyJSON: &models.UserGroup{
|
||||
GroupName: "vsphere.local\\guest",
|
||||
GroupType: common.HTTPGroupType,
|
||||
},
|
||||
credential: admin,
|
||||
},
|
||||
code: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/usergroups",
|
||||
bodyJSON: &models.UserGroup{
|
||||
GroupName: "vsphere.local\\guest",
|
||||
GroupType: common.HTTPGroupType,
|
||||
},
|
||||
credential: admin,
|
||||
},
|
||||
@ -118,7 +143,7 @@ func TestUserGroupAPI_Put(t *testing.T) {
|
||||
groupID, err := group.AddUserGroup(models.UserGroup{
|
||||
GroupName: "harbor_group",
|
||||
LdapGroupDN: "cn=harbor_groups,ou=groups,dc=example,dc=com",
|
||||
GroupType: common.LdapGroupType,
|
||||
GroupType: common.LDAPGroupType,
|
||||
})
|
||||
defer group.DeleteUserGroup(groupID)
|
||||
|
||||
@ -149,6 +174,19 @@ func TestUserGroupAPI_Put(t *testing.T) {
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("/api/usergroups/%d", groupID),
|
||||
bodyJSON: &models.UserGroup{
|
||||
GroupName: "my_group",
|
||||
GroupType: common.HTTPGroupType,
|
||||
},
|
||||
credential: admin,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
@ -16,18 +16,24 @@ package authproxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"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/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"github.com/goharbor/harbor/src/pkg/authproxy"
|
||||
k8s_api_v1beta1 "k8s.io/api/authentication/v1beta1"
|
||||
)
|
||||
|
||||
const refreshDuration = 2 * time.Second
|
||||
@ -45,11 +51,16 @@ var insecureTransport = &http.Transport{
|
||||
type Auth struct {
|
||||
auth.DefaultAuthenticateHelper
|
||||
sync.Mutex
|
||||
Endpoint string
|
||||
SkipCertVerify bool
|
||||
AlwaysOnboard bool
|
||||
settingTimeStamp time.Time
|
||||
client *http.Client
|
||||
Endpoint string
|
||||
TokenReviewEndpoint string
|
||||
SkipCertVerify bool
|
||||
SkipSearch bool
|
||||
settingTimeStamp time.Time
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type session struct {
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
}
|
||||
|
||||
// Authenticate issues http POST request to Endpoint if it returns 200 the authentication is considered success.
|
||||
@ -72,7 +83,39 @@ func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return &models.User{Username: m.Principal}, nil
|
||||
user := &models.User{Username: m.Principal}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Warningf("Failed to read response body, error: %v", err)
|
||||
return nil, auth.ErrAuth{}
|
||||
}
|
||||
s := session{}
|
||||
err = json.Unmarshal(data, &s)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read session %v", err)
|
||||
}
|
||||
|
||||
reviewResponse, err := a.tokenReview(s.SessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if reviewResponse == nil {
|
||||
return nil, auth.ErrAuth{}
|
||||
}
|
||||
|
||||
// Attach user group ID information
|
||||
ugList := reviewResponse.Status.User.Groups
|
||||
log.Debugf("user groups %+v", ugList)
|
||||
if len(ugList) > 0 {
|
||||
groupIDList, err := group.GetGroupIDByGroupName(ugList, common.HTTPGroupType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("current user's group ID list is %+v", groupIDList)
|
||||
user.GroupIDs = groupIDList
|
||||
}
|
||||
return user, nil
|
||||
|
||||
} else if resp.StatusCode == http.StatusUnauthorized {
|
||||
return nil, auth.ErrAuth{}
|
||||
} else {
|
||||
@ -81,10 +124,19 @@ func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
log.Warningf("Failed to read response body, error: %v", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to authenticate, status code: %d, text: %s", resp.StatusCode, string(data))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (a *Auth) tokenReview(sessionID string) (*k8s_api_v1beta1.TokenReview, error) {
|
||||
httpAuthProxySetting, err := config.HTTPAuthProxySetting()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authproxy.TokenReview(sessionID, httpAuthProxySetting)
|
||||
}
|
||||
|
||||
// OnBoardUser delegates to dao pkg to insert/update data in DB.
|
||||
func (a *Auth) OnBoardUser(u *models.User) error {
|
||||
return dao.OnBoardUser(u)
|
||||
@ -102,14 +154,14 @@ func (a *Auth) PostAuthenticate(u *models.User) error {
|
||||
}
|
||||
|
||||
// SearchUser returns nil as authproxy does not have such capability.
|
||||
// When AlwaysOnboard is set it always return the default model.
|
||||
// When SkipSearch is set it always return the default model.
|
||||
func (a *Auth) SearchUser(username string) (*models.User, error) {
|
||||
err := a.ensure()
|
||||
if err != nil {
|
||||
log.Warningf("Failed to refresh configuration for HTTP Auth Proxy Authenticator, error: %v, the default settings will be used", err)
|
||||
}
|
||||
var u *models.User
|
||||
if a.AlwaysOnboard {
|
||||
if a.SkipSearch {
|
||||
u = &models.User{Username: username}
|
||||
if err := a.fillInModel(u); err != nil {
|
||||
return nil, err
|
||||
@ -118,6 +170,35 @@ func (a *Auth) SearchUser(username string) (*models.User, error) {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// SearchGroup search group exist in the authentication provider, for HTTP auth, if SkipSearch is true, it assume this group exist in authentication provider.
|
||||
func (a *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) {
|
||||
err := a.ensure()
|
||||
if err != nil {
|
||||
log.Warningf("Failed to refresh configuration for HTTP Auth Proxy Authenticator, error: %v, the default settings will be used", err)
|
||||
}
|
||||
var ug *models.UserGroup
|
||||
if a.SkipSearch {
|
||||
ug = &models.UserGroup{
|
||||
GroupName: groupKey,
|
||||
GroupType: common.HTTPGroupType,
|
||||
}
|
||||
return ug, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// OnBoardGroup create user group entity in Harbor DB, altGroupName is not used.
|
||||
func (a *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
|
||||
// if group name provided, on board the user group
|
||||
userGroup := &models.UserGroup{GroupName: u.GroupName, GroupType: common.HTTPGroupType}
|
||||
err := group.OnBoardUserGroup(u, "GroupName", "GroupType")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.ID = userGroup.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auth) fillInModel(u *models.User) error {
|
||||
if strings.TrimSpace(u.Username) == "" {
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
@ -145,8 +226,9 @@ func (a *Auth) ensure() error {
|
||||
return err
|
||||
}
|
||||
a.Endpoint = setting.Endpoint
|
||||
a.TokenReviewEndpoint = setting.TokenReviewEndpoint
|
||||
a.SkipCertVerify = !setting.VerifyCert
|
||||
a.AlwaysOnboard = setting.AlwaysOnBoard
|
||||
a.SkipSearch = setting.SkipSearch
|
||||
}
|
||||
if a.SkipCertVerify {
|
||||
a.client.Transport = insecureTransport
|
||||
|
@ -15,18 +15,20 @@
|
||||
package authproxy
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
cut "github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/auth/authproxy/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var mockSvr *httptest.Server
|
||||
@ -42,15 +44,16 @@ func TestMain(m *testing.M) {
|
||||
mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"})
|
||||
defer mockSvr.Close()
|
||||
a = &Auth{
|
||||
Endpoint: mockSvr.URL + "/test/login",
|
||||
SkipCertVerify: true,
|
||||
Endpoint: mockSvr.URL + "/test/login",
|
||||
TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview",
|
||||
SkipCertVerify: true,
|
||||
// So it won't require mocking the cfgManager
|
||||
settingTimeStamp: time.Now(),
|
||||
}
|
||||
conf := map[string]interface{}{
|
||||
common.HTTPAuthProxyEndpoint: "dummy",
|
||||
common.HTTPAuthProxyTokenReviewEndpoint: "dummy",
|
||||
common.HTTPAuthProxyVerifyCert: "false",
|
||||
common.HTTPAuthProxyEndpoint: a.Endpoint,
|
||||
common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint,
|
||||
common.HTTPAuthProxyVerifyCert: !a.SkipCertVerify,
|
||||
}
|
||||
|
||||
config.InitWithSettings(conf)
|
||||
@ -64,6 +67,10 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
func TestAuth_Authenticate(t *testing.T) {
|
||||
groupIDs, err := group.GetGroupIDByGroupName([]string{"vsphere.local\\users", "vsphere.local\\administrators"}, common.HTTPGroupType)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to get groupIDs")
|
||||
}
|
||||
t.Log("auth endpoint: ", a.Endpoint)
|
||||
type output struct {
|
||||
user models.User
|
||||
@ -80,6 +87,7 @@ func TestAuth_Authenticate(t *testing.T) {
|
||||
expect: output{
|
||||
user: models.User{
|
||||
Username: "jt",
|
||||
GroupIDs: groupIDs,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
@ -92,6 +100,7 @@ func TestAuth_Authenticate(t *testing.T) {
|
||||
expect: output{
|
||||
user: models.User{
|
||||
Username: "Admin@vsphere.local",
|
||||
GroupIDs: groupIDs,
|
||||
// Email: "Admin@placeholder.com",
|
||||
// Password: pwd,
|
||||
// Comment: fmt.Sprintf(cmtTmpl, path.Join(mockSvr.URL, "/test/login")),
|
||||
|
@ -41,9 +41,20 @@ func (ah *authHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
type reviewTokenHandler struct {
|
||||
}
|
||||
|
||||
func (rth *reviewTokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "", http.StatusMethodNotAllowed)
|
||||
}
|
||||
rw.Write([]byte(`{"apiVersion": "authentication.k8s.io/v1beta1", "kind": "TokenReview", "status": {"authenticated": true, "user": {"username": "administrator@vsphere.local", "groups": ["vsphere.local\\users", "vsphere.local\\administrators", "vsphere.local\\caadmins", "vsphere.local\\systemconfiguration.bashshelladministrators", "vsphere.local\\systemconfiguration.administrators", "vsphere.local\\licenseservice.administrators", "vsphere.local\\everyone"], "extra": {"method": ["basic"]}}}}`))
|
||||
}
|
||||
|
||||
// NewMockServer creates the mock server for testing
|
||||
func NewMockServer(creds map[string]string) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/test/login", &authHandler{m: creds})
|
||||
mux.Handle("/test/tokenreview", &reviewTokenHandler{})
|
||||
return httptest.NewTLSServer(mux)
|
||||
}
|
||||
|
@ -20,11 +20,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
goldap "gopkg.in/ldap.v2"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
ldapUtils "github.com/goharbor/harbor/src/common/utils/ldap"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
@ -79,7 +79,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
u.Username = ldapUsers[0].Username
|
||||
u.Email = strings.TrimSpace(ldapUsers[0].Email)
|
||||
u.Realname = ldapUsers[0].Realname
|
||||
userGroups := make([]*models.UserGroup, 0)
|
||||
ugIDs := []int{}
|
||||
|
||||
dn := ldapUsers[0].DN
|
||||
if err = ldapSession.Bind(dn, m.Password); err != nil {
|
||||
@ -95,6 +95,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
for _, groupDN := range ldapUsers[0].GroupDNList {
|
||||
|
||||
groupDN = utils.TrimLower(groupDN)
|
||||
// Attach LDAP group admin
|
||||
if len(groupAdminDN) > 0 && groupAdminDN == groupDN {
|
||||
u.HasAdminRole = true
|
||||
}
|
||||
@ -103,16 +104,16 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
GroupType: 1,
|
||||
LdapGroupDN: groupDN,
|
||||
}
|
||||
userGroupList, err := group.QueryUserGroup(userGroupQuery)
|
||||
userGroups, err := group.QueryUserGroup(userGroupQuery)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(userGroupList) == 0 {
|
||||
if len(userGroups) == 0 {
|
||||
continue
|
||||
}
|
||||
userGroups = append(userGroups, userGroupList[0])
|
||||
ugIDs = append(ugIDs, userGroups[0].ID)
|
||||
}
|
||||
u.GroupList = userGroups
|
||||
u.GroupIDs = ugIDs
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
@ -204,7 +205,7 @@ func (l *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
|
||||
if len(altGroupName) > 0 {
|
||||
u.GroupName = altGroupName
|
||||
}
|
||||
u.GroupType = common.LdapGroupType
|
||||
u.GroupType = common.LDAPGroupType
|
||||
// Check duplicate LDAP DN in usergroup, if usergroup exist, return error
|
||||
userGroupList, err := group.QueryUserGroup(models.UserGroup{LdapGroupDN: u.LdapGroupDN})
|
||||
if err != nil {
|
||||
|
@ -55,7 +55,7 @@ var ldapTestConfig = map[string]interface{}{
|
||||
common.LDAPGroupBaseDN: "dc=example,dc=com",
|
||||
common.LDAPGroupAttributeName: "cn",
|
||||
common.LDAPGroupSearchScope: 2,
|
||||
common.LdapGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
common.LDAPGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@ -92,8 +92,8 @@ func TestMain(m *testing.M) {
|
||||
"delete from user_group",
|
||||
"delete from project_member",
|
||||
}
|
||||
dao.PrepareTestData(clearSqls, initSqls)
|
||||
|
||||
dao.ExecuteBatchSQL(initSqls)
|
||||
defer dao.ExecuteBatchSQL(clearSqls)
|
||||
retCode := m.Run()
|
||||
os.Exit(retCode)
|
||||
}
|
||||
@ -405,6 +405,7 @@ func TestAddProjectMemberWithLdapGroup(t *testing.T) {
|
||||
ProjectID: currentProject.ProjectID,
|
||||
MemberGroup: models.UserGroup{
|
||||
LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
GroupType: 1,
|
||||
},
|
||||
Role: models.PROJECTADMIN,
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ func LDAPGroupConf() (*models.LdapGroupConf, error) {
|
||||
LdapGroupFilter: cfgMgr.Get(common.LDAPGroupSearchFilter).GetString(),
|
||||
LdapGroupNameAttribute: cfgMgr.Get(common.LDAPGroupAttributeName).GetString(),
|
||||
LdapGroupSearchScope: cfgMgr.Get(common.LDAPGroupSearchScope).GetInt(),
|
||||
LdapGroupAdminDN: cfgMgr.Get(common.LdapGroupAdminDn).GetString(),
|
||||
LdapGroupAdminDN: cfgMgr.Get(common.LDAPGroupAdminDn).GetString(),
|
||||
LdapGroupMembershipAttribute: cfgMgr.Get(common.LDAPGroupMembershipAttribute).GetString(),
|
||||
}, nil
|
||||
}
|
||||
@ -482,7 +482,7 @@ func HTTPAuthProxySetting() (*models.HTTPAuthProxy, error) {
|
||||
Endpoint: cfgMgr.Get(common.HTTPAuthProxyEndpoint).GetString(),
|
||||
TokenReviewEndpoint: cfgMgr.Get(common.HTTPAuthProxyTokenReviewEndpoint).GetString(),
|
||||
VerifyCert: cfgMgr.Get(common.HTTPAuthProxyVerifyCert).GetBool(),
|
||||
AlwaysOnBoard: cfgMgr.Get(common.HTTPAuthProxyAlwaysOnboard).GetBool(),
|
||||
SkipSearch: cfgMgr.Get(common.HTTPAuthProxySkipSearch).GetBool(),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
@ -228,17 +229,17 @@ func TestConfigureValue_GetMap(t *testing.T) {
|
||||
|
||||
func TestHTTPAuthProxySetting(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
common.HTTPAuthProxyAlwaysOnboard: "true",
|
||||
common.HTTPAuthProxyVerifyCert: "true",
|
||||
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
|
||||
common.HTTPAuthProxySkipSearch: "true",
|
||||
common.HTTPAuthProxyVerifyCert: "true",
|
||||
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
|
||||
}
|
||||
InitWithSettings(m)
|
||||
v, e := HTTPAuthProxySetting()
|
||||
assert.Nil(t, e)
|
||||
assert.Equal(t, *v, models.HTTPAuthProxy{
|
||||
Endpoint: "https://auth.proxy/suffix",
|
||||
AlwaysOnBoard: true,
|
||||
VerifyCert: true,
|
||||
Endpoint: "https://auth.proxy/suffix",
|
||||
SkipSearch: true,
|
||||
VerifyCert: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -41,15 +41,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/admiral"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
k8s_api_v1beta1 "k8s.io/api/authentication/v1beta1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"github.com/goharbor/harbor/src/pkg/authproxy"
|
||||
)
|
||||
|
||||
// ContextValueKey for content value
|
||||
@ -229,8 +221,10 @@ type oidcCliReqCtxModifier struct{}
|
||||
|
||||
func (oc *oidcCliReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
path := ctx.Request.URL.Path
|
||||
if path != "/service/token" && !strings.HasPrefix(path, "/chartrepo/") {
|
||||
log.Debug("OIDC CLI modifer only handles request by docker CLI or helm CLI")
|
||||
if path != "/service/token" &&
|
||||
!strings.HasPrefix(path, "/chartrepo/") &&
|
||||
!strings.HasPrefix(path, "/api/chartrepo/") {
|
||||
log.Debug("OIDC CLI modifier only handles request by docker CLI or helm CLI")
|
||||
return false
|
||||
}
|
||||
if ctx.Request.Context().Value(AuthModeKey).(string) != common.OIDCAuth {
|
||||
@ -319,60 +313,17 @@ func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
log.Errorf("User name %s doesn't meet the auth proxy name pattern", proxyUserName)
|
||||
return false
|
||||
}
|
||||
|
||||
httpAuthProxyConf, err := config.HTTPAuthProxySetting()
|
||||
if err != nil {
|
||||
log.Errorf("fail to get auth proxy settings, %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Init auth client with the auth proxy endpoint.
|
||||
authClientCfg := &rest.Config{
|
||||
Host: httpAuthProxyConf.TokenReviewEndpoint,
|
||||
ContentConfig: rest.ContentConfig{
|
||||
GroupVersion: &schema.GroupVersion{},
|
||||
NegotiatedSerializer: serializer.DirectCodecFactory{CodecFactory: scheme.Codecs},
|
||||
},
|
||||
BearerToken: proxyPwd,
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
Insecure: !httpAuthProxyConf.VerifyCert,
|
||||
},
|
||||
}
|
||||
authClient, err := rest.RESTClientFor(authClientCfg)
|
||||
tokenReviewResponse, err := authproxy.TokenReview(proxyPwd, httpAuthProxyConf)
|
||||
if err != nil {
|
||||
log.Errorf("fail to create auth client, %v", err)
|
||||
log.Errorf("fail to review token, %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Do auth with the token.
|
||||
tokenReviewRequest := &k8s_api_v1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "TokenReview",
|
||||
APIVersion: "authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Spec: k8s_api_v1beta1.TokenReviewSpec{
|
||||
Token: proxyPwd,
|
||||
},
|
||||
}
|
||||
res := authClient.Post().Body(tokenReviewRequest).Do()
|
||||
err = res.Error()
|
||||
if err != nil {
|
||||
log.Errorf("fail to POST auth request, %v", err)
|
||||
return false
|
||||
}
|
||||
resRaw, err := res.Raw()
|
||||
if err != nil {
|
||||
log.Errorf("fail to get raw data of token review, %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the auth response, check the user name and authenticated status.
|
||||
tokenReviewResponse := &k8s_api_v1beta1.TokenReview{}
|
||||
err = json.Unmarshal(resRaw, &tokenReviewResponse)
|
||||
if err != nil {
|
||||
log.Errorf("fail to decode token review, %v", err)
|
||||
return false
|
||||
}
|
||||
if !tokenReviewResponse.Status.Authenticated {
|
||||
log.Errorf("fail to auth user: %s", rawUserName)
|
||||
return false
|
||||
|
@ -16,8 +16,6 @@ package filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||
"github.com/stretchr/testify/require"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@ -27,6 +25,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
"github.com/astaxie/beego/session"
|
||||
@ -241,7 +242,7 @@ func TestAuthProxyReqCtxModifier(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
c := map[string]interface{}{
|
||||
common.HTTPAuthProxyAlwaysOnboard: "true",
|
||||
common.HTTPAuthProxySkipSearch: "true",
|
||||
common.HTTPAuthProxyVerifyCert: "false",
|
||||
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
|
||||
common.HTTPAuthProxyTokenReviewEndpoint: server.URL,
|
||||
@ -253,7 +254,7 @@ func TestAuthProxyReqCtxModifier(t *testing.T) {
|
||||
assert.Nil(t, e)
|
||||
assert.Equal(t, *v, models.HTTPAuthProxy{
|
||||
Endpoint: "https://auth.proxy/suffix",
|
||||
AlwaysOnBoard: true,
|
||||
SkipSearch: true,
|
||||
VerifyCert: false,
|
||||
TokenReviewEndpoint: server.URL,
|
||||
})
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
errutil "github.com/goharbor/harbor/src/common/utils/error"
|
||||
@ -132,19 +131,16 @@ func (d *driver) Update(projectIDOrName interface{},
|
||||
func (d *driver) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
|
||||
var total int64
|
||||
var projects []*models.Project
|
||||
var groupDNCondition string
|
||||
|
||||
// List with LDAP group projects
|
||||
var groupIDs []int
|
||||
if query != nil && query.Member != nil {
|
||||
groupDNCondition = group.GetGroupDNQueryCondition(query.Member.GroupList)
|
||||
groupIDs = query.Member.GroupIDs
|
||||
}
|
||||
|
||||
count, err := dao.GetTotalGroupProjects(groupDNCondition, query)
|
||||
count, err := dao.GetTotalGroupProjects(groupIDs, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total = int64(count)
|
||||
projects, err = dao.GetGroupProjects(groupDNCondition, query)
|
||||
projects, err = dao.GetGroupProjects(groupIDs, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ package promgr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
@ -44,6 +45,7 @@ type defaultProjectManager struct {
|
||||
pmsDriver pmsdriver.PMSDriver
|
||||
metaMgrEnabled bool // if metaMgrEnabled is enabled, metaMgr will be used to CURD metadata
|
||||
metaMgr metamgr.ProjectMetadataManager
|
||||
whitelistMgr whitelist.Manager
|
||||
}
|
||||
|
||||
// NewDefaultProjectManager returns an instance of defaultProjectManager,
|
||||
@ -56,6 +58,7 @@ func NewDefaultProjectManager(driver pmsdriver.PMSDriver, metaMgrEnabled bool) P
|
||||
}
|
||||
if metaMgrEnabled {
|
||||
mgr.metaMgr = metamgr.NewDefaultProjectMetadataManager()
|
||||
mgr.whitelistMgr = whitelist.NewDefaultManager()
|
||||
}
|
||||
return mgr
|
||||
}
|
||||
@ -77,6 +80,11 @@ func (d *defaultProjectManager) Get(projectIDOrName interface{}) (*models.Projec
|
||||
for k, v := range meta {
|
||||
project.Metadata[k] = v
|
||||
}
|
||||
wl, err := d.whitelistMgr.Get(project.ProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
project.CVEWhitelist = *wl
|
||||
}
|
||||
return project, nil
|
||||
}
|
||||
@ -85,9 +93,12 @@ func (d *defaultProjectManager) Create(project *models.Project) (int64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(project.Metadata) > 0 && d.metaMgrEnabled {
|
||||
if err = d.metaMgr.Add(id, project.Metadata); err != nil {
|
||||
log.Errorf("failed to add metadata for project %s: %v", project.Name, err)
|
||||
if d.metaMgrEnabled {
|
||||
d.whitelistMgr.CreateEmpty(project.ProjectID)
|
||||
if len(project.Metadata) > 0 {
|
||||
if err = d.metaMgr.Add(id, project.Metadata); err != nil {
|
||||
log.Errorf("failed to add metadata for project %s: %v", project.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
@ -110,37 +121,40 @@ func (d *defaultProjectManager) Delete(projectIDOrName interface{}) error {
|
||||
}
|
||||
|
||||
func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *models.Project) error {
|
||||
if len(project.Metadata) > 0 && d.metaMgrEnabled {
|
||||
pro, err := d.Get(projectIDOrName)
|
||||
if err != nil {
|
||||
pro, err := d.Get(projectIDOrName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pro == nil {
|
||||
return fmt.Errorf("project %v not found", projectIDOrName)
|
||||
}
|
||||
// TODO transaction?
|
||||
if d.metaMgrEnabled {
|
||||
if err := d.whitelistMgr.Set(pro.ProjectID, project.CVEWhitelist); err != nil {
|
||||
return err
|
||||
}
|
||||
if pro == nil {
|
||||
return fmt.Errorf("project %v not found", projectIDOrName)
|
||||
}
|
||||
|
||||
// TODO transaction?
|
||||
metaNeedUpdated := map[string]string{}
|
||||
metaNeedCreated := map[string]string{}
|
||||
if pro.Metadata == nil {
|
||||
pro.Metadata = map[string]string{}
|
||||
}
|
||||
for key, value := range project.Metadata {
|
||||
_, exist := pro.Metadata[key]
|
||||
if exist {
|
||||
metaNeedUpdated[key] = value
|
||||
} else {
|
||||
metaNeedCreated[key] = value
|
||||
if len(project.Metadata) > 0 {
|
||||
metaNeedUpdated := map[string]string{}
|
||||
metaNeedCreated := map[string]string{}
|
||||
if pro.Metadata == nil {
|
||||
pro.Metadata = map[string]string{}
|
||||
}
|
||||
for key, value := range project.Metadata {
|
||||
_, exist := pro.Metadata[key]
|
||||
if exist {
|
||||
metaNeedUpdated[key] = value
|
||||
} else {
|
||||
metaNeedCreated[key] = value
|
||||
}
|
||||
}
|
||||
if err = d.metaMgr.Add(pro.ProjectID, metaNeedCreated); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = d.metaMgr.Update(pro.ProjectID, metaNeedUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = d.metaMgr.Add(pro.ProjectID, metaNeedCreated); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = d.metaMgr.Update(pro.ProjectID, metaNeedUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return d.pmsDriver.Update(projectIDOrName, project)
|
||||
}
|
||||
|
||||
@ -179,6 +193,7 @@ func (d *defaultProjectManager) List(query *models.ProjectQueryParam) (*models.P
|
||||
project.Metadata = meta
|
||||
}
|
||||
}
|
||||
// the whitelist is not populated deliberately
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
@ -166,9 +166,10 @@ func TestPMSPolicyChecker(t *testing.T) {
|
||||
Name: name,
|
||||
OwnerID: 1,
|
||||
Metadata: map[string]string{
|
||||
models.ProMetaEnableContentTrust: "true",
|
||||
models.ProMetaPreventVul: "true",
|
||||
models.ProMetaSeverity: "low",
|
||||
models.ProMetaEnableContentTrust: "true",
|
||||
models.ProMetaPreventVul: "true",
|
||||
models.ProMetaSeverity: "low",
|
||||
models.ProMetaReuseSysCVEWhitelist: "false",
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
@ -180,9 +181,10 @@ func TestPMSPolicyChecker(t *testing.T) {
|
||||
|
||||
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low")
|
||||
assert.True(t, contentTrustFlag)
|
||||
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low")
|
||||
projectVulnerableEnabled, projectVulnerableSeverity, wl := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low")
|
||||
assert.True(t, projectVulnerableEnabled)
|
||||
assert.Equal(t, projectVulnerableSeverity, models.SevLow)
|
||||
assert.Empty(t, wl.Items)
|
||||
}
|
||||
|
||||
func TestMatchNotaryDigest(t *testing.T) {
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||
"github.com/goharbor/harbor/src/pkg/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
@ -82,7 +83,7 @@ type policyChecker interface {
|
||||
// contentTrustEnabled returns whether a project has enabled content trust.
|
||||
contentTrustEnabled(name string) bool
|
||||
// vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity.
|
||||
vulnerablePolicy(name string) (bool, models.Severity)
|
||||
vulnerablePolicy(name string) (bool, models.Severity, models.CVEWhitelist)
|
||||
}
|
||||
|
||||
type pmsPolicyChecker struct {
|
||||
@ -97,13 +98,28 @@ func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool {
|
||||
}
|
||||
return project.ContentTrustEnabled()
|
||||
}
|
||||
func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
|
||||
func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity, models.CVEWhitelist) {
|
||||
project, err := pc.pm.Get(name)
|
||||
wl := models.CVEWhitelist{}
|
||||
if err != nil {
|
||||
log.Errorf("Unexpected error when getting the project, error: %v", err)
|
||||
return true, models.SevUnknown
|
||||
return true, models.SevUnknown, wl
|
||||
}
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity())
|
||||
mgr := whitelist.NewDefaultManager()
|
||||
if project.ReuseSysCVEWhitelist() {
|
||||
w, err := mgr.GetSys()
|
||||
if err != nil {
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
|
||||
}
|
||||
wl = *w
|
||||
} else {
|
||||
w, err := mgr.Get(project.ProjectID)
|
||||
if err != nil {
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
|
||||
}
|
||||
wl = *w
|
||||
}
|
||||
return project.VulPrevented(), clair.ParseClairSev(project.Severity()), wl
|
||||
}
|
||||
|
||||
// newPMSPolicyChecker returns an instance of an pmsPolicyChecker
|
||||
@ -298,25 +314,18 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName)
|
||||
projectVulnerableEnabled, projectVulnerableSeverity, wl := getPolicyChecker().vulnerablePolicy(img.projectName)
|
||||
if !projectVulnerableEnabled {
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
// TODO: Get whitelist based on project setting
|
||||
wl, err := dao.GetSysCVEWhitelist()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get the whitelist, error: %v", err)
|
||||
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get CVE whitelist."), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
vl, err := scan.VulnListByDigest(img.digest)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get the vulnerability list, error: %v", err)
|
||||
http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get vulnerabilities."), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
filtered := vl.ApplyWhitelist(*wl)
|
||||
filtered := vl.ApplyWhitelist(wl)
|
||||
msg := vh.filterMsg(img, filtered)
|
||||
log.Info(msg)
|
||||
if int(vl.Severity()) >= int(projectVulnerableSeverity) {
|
||||
|
65
src/pkg/authproxy/http.go
Normal file
65
src/pkg/authproxy/http.go
Normal file
@ -0,0 +1,65 @@
|
||||
package authproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
k8s_api_v1beta1 "k8s.io/api/authentication/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// TokenReview ...
|
||||
func TokenReview(sessionID string, authProxyConfig *models.HTTPAuthProxy) (*k8s_api_v1beta1.TokenReview, error) {
|
||||
|
||||
// Init auth client with the auth proxy endpoint.
|
||||
authClientCfg := &rest.Config{
|
||||
Host: authProxyConfig.TokenReviewEndpoint,
|
||||
ContentConfig: rest.ContentConfig{
|
||||
GroupVersion: &schema.GroupVersion{},
|
||||
NegotiatedSerializer: serializer.DirectCodecFactory{CodecFactory: scheme.Codecs},
|
||||
},
|
||||
BearerToken: sessionID,
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
Insecure: !authProxyConfig.VerifyCert,
|
||||
},
|
||||
}
|
||||
authClient, err := rest.RESTClientFor(authClientCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do auth with the token.
|
||||
tokenReviewRequest := &k8s_api_v1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "TokenReview",
|
||||
APIVersion: "authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Spec: k8s_api_v1beta1.TokenReviewSpec{
|
||||
Token: sessionID,
|
||||
},
|
||||
}
|
||||
res := authClient.Post().Body(tokenReviewRequest).Do()
|
||||
err = res.Error()
|
||||
if err != nil {
|
||||
log.Errorf("fail to POST auth request, %v", err)
|
||||
return nil, err
|
||||
}
|
||||
resRaw, err := res.Raw()
|
||||
if err != nil {
|
||||
log.Errorf("fail to get raw data of token review, %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// Parse the auth response, check the user name and authenticated status.
|
||||
tokenReviewResponse := &k8s_api_v1beta1.TokenReview{}
|
||||
err = json.Unmarshal(resRaw, &tokenReviewResponse)
|
||||
if err != nil {
|
||||
log.Errorf("fail to decode token review, %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return tokenReviewResponse, nil
|
||||
|
||||
}
|
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))
|
||||
}
|
||||
}
|
||||
}
|
@ -90,7 +90,7 @@ export class Configuration {
|
||||
http_authproxy_endpoint?: StringValueItem;
|
||||
http_authproxy_tokenreview_endpoint?: StringValueItem;
|
||||
http_authproxy_verify_cert?: BoolValueItem;
|
||||
http_authproxy_always_onboard?: BoolValueItem;
|
||||
http_authproxy_skip_search?: BoolValueItem;
|
||||
oidc_name?: StringValueItem;
|
||||
oidc_endpoint?: StringValueItem;
|
||||
oidc_client_id?: StringValueItem;
|
||||
@ -141,7 +141,7 @@ export class Configuration {
|
||||
this.http_authproxy_endpoint = new StringValueItem("", true);
|
||||
this.http_authproxy_tokenreview_endpoint = new StringValueItem("", true);
|
||||
this.http_authproxy_verify_cert = new BoolValueItem(false, true);
|
||||
this.http_authproxy_always_onboard = new BoolValueItem(false, true);
|
||||
this.http_authproxy_skip_search = new BoolValueItem(false, true);
|
||||
this.oidc_name = new StringValueItem('', true);
|
||||
this.oidc_endpoint = new StringValueItem('', true);
|
||||
this.oidc_client_id = new StringValueItem('', true);
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
ScanningResultDefaultService,
|
||||
SystemInfoService,
|
||||
SystemInfoDefaultService,
|
||||
SystemInfo
|
||||
SystemInfo, SystemCVEWhitelist
|
||||
} from '../service/index';
|
||||
import { Configuration } from './config';
|
||||
import { of } from 'rxjs';
|
||||
@ -56,7 +56,12 @@ describe('RegistryConfigComponent (inline template)', () => {
|
||||
"harbor_version": "v1.1.1-rc1-160-g565110d",
|
||||
"next_scan_all": 0
|
||||
};
|
||||
|
||||
let mockSystemWhitelist: SystemCVEWhitelist = {
|
||||
"expires_at": 1561996800,
|
||||
"id": 1,
|
||||
"items": [],
|
||||
"project_id": 0
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@ -90,7 +95,7 @@ describe('RegistryConfigComponent (inline template)', () => {
|
||||
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
|
||||
spy = spyOn(cfgService, 'getConfigurations').and.returnValue(of(mockConfig));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValue(of(mockSystemInfo));
|
||||
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemWhitelist').and.returnValue(of(mockSystemWhitelist));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@ -4,23 +4,27 @@
|
||||
<div class="form-group">
|
||||
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}</label>
|
||||
<div class="select">
|
||||
<select id="proCreation" name="proCreation" [(ngModel)]="systemSettings.project_creation_restriction.value"
|
||||
[disabled]="disabled(systemSettings.project_creation_restriction)">
|
||||
<select id="proCreation" name="proCreation"
|
||||
[(ngModel)]="systemSettings.project_creation_restriction.value"
|
||||
[disabled]="disabled(systemSettings.project_creation_restriction)">
|
||||
<option value="everyone">{{'CONFIG.PRO_CREATION_EVERYONE' | translate }}</option>
|
||||
<option value="adminonly">{{'CONFIG.PRO_CREATION_ADMIN' | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
|
||||
class="tooltip tooltip-lg tooltip-top-right">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.PRO_CREATION_RESTRICTION' | translate}}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
|
||||
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
|
||||
[class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
|
||||
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="systemSettings.token_expiration.value"
|
||||
required pattern="^[1-9]{1}[0-9]*$" id="tokenExpiration" size="20" [disabled]="!editable">
|
||||
<label for="tokenExpiration" aria-haspopup="true" role="tooltip"
|
||||
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
|
||||
[class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
|
||||
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel"
|
||||
[(ngModel)]="systemSettings.token_expiration.value"
|
||||
required pattern="^[1-9]{1}[0-9]*$" id="tokenExpiration" size="20" [disabled]="!editable">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
|
||||
</span>
|
||||
@ -32,10 +36,13 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="robotTokenExpiration" class="required">{{'ROBOT_ACCOUNT.TOKEN_EXPIRATION' | translate}}</label>
|
||||
<label for="robotTokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
|
||||
[class.invalid]="robotTokenExpirationInput.invalid && (robotTokenExpirationInput.dirty || robotTokenExpirationInput.touched)">
|
||||
<input name="robotTokenExpiration" type="text" #robotTokenExpirationInput="ngModel" (ngModelChange)="changeToken($event)" [(ngModel)]="robotTokenExpiration"
|
||||
required pattern="^[1-9]{1}[0-9]*$" id="robotTokenExpiration" size="20" [disabled]="!robotExpirationEditable">
|
||||
<label for="robotTokenExpiration" aria-haspopup="true" role="tooltip"
|
||||
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
|
||||
[class.invalid]="robotTokenExpirationInput.invalid && (robotTokenExpirationInput.dirty || robotTokenExpirationInput.touched)">
|
||||
<input name="robotTokenExpiration" type="text" #robotTokenExpirationInput="ngModel"
|
||||
(ngModelChange)="changeToken($event)" [(ngModel)]="robotTokenExpiration"
|
||||
required pattern="^[1-9]{1}[0-9]*$" id="robotTokenExpiration" size="20"
|
||||
[disabled]="!robotExpirationEditable">
|
||||
<span class="tooltip-content">
|
||||
{{'ROBOT_ACCOUNT.NUMBER_REQUIRED' | translate}}
|
||||
</span>
|
||||
@ -56,22 +63,94 @@
|
||||
<div *ngIf="!withAdmiral" class="form-group">
|
||||
<label for="repoReadOnly">{{'CONFIG.REPO_READ_ONLY' | translate}}</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox name="repoReadOnly" id="repoReadOnly" [ngModel]="systemSettings.read_only.value"
|
||||
(ngModelChange)="setRepoReadOnlyValue($event)" />
|
||||
<input type="checkbox" clrCheckbox name="repoReadOnly" id="repoReadOnly"
|
||||
[ngModel]="systemSettings.read_only.value"
|
||||
(ngModelChange)="setRepoReadOnlyValue($event)"/>
|
||||
<label>
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right read-tooltip">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
|
||||
class="tooltip tooltip-top-right read-tooltip">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.REPO_TOOLTIP' | translate}}</span>
|
||||
</a>
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="withClair">
|
||||
<label for="systemWhitelist">{{'CVE_WHITELIST.DEPLOYMENT_SECURITY'|translate}}</label>
|
||||
<div class="form-content w-100">
|
||||
<div>
|
||||
<div>
|
||||
<span class="title">{{'CVE_WHITELIST.CVE_WHITELIST'|translate}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{'CVE_WHITELIST.SYS_WHITELIST_EXPLAIN'|translate}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{'CVE_WHITELIST.ADD_SYS'|translate}}</span>
|
||||
</div>
|
||||
<div *ngIf="hasExpired">
|
||||
<span class="label label-warning">{{'CVE_WHITELIST.WARNING_SYS'|translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row width-70per">
|
||||
<div class="clr-col position-relative">
|
||||
<div>
|
||||
<button (click)="showAddModal=!showAddModal"
|
||||
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
|
||||
</div>
|
||||
<div class="add-modal" *ngIf="showAddModal">
|
||||
<div>
|
||||
<clr-textarea-container>
|
||||
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
|
||||
<textarea class="w-100 font-italic" clrTextarea [(ngModel)]="cveIds"
|
||||
name="cveIds"></textarea>
|
||||
<clr-control-helper>{{'CVE_WHITELIST.HELP'|translate}}</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
</div>
|
||||
<div>
|
||||
<button [disabled]="isDisabled()" (click)="addToSystemWhitelist()"
|
||||
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="whitelist-window">
|
||||
<li *ngIf="systemWhitelist?.items?.length<1"
|
||||
class="none">{{'CVE_WHITELIST.NONE'|translate}}</li>
|
||||
<li *ngFor="let item of systemWhitelist?.items;let i = index;">{{item.cve_id}}
|
||||
<clr-icon (click)="deleteItem(i)" class="float-lg-right margin-top-4"
|
||||
shape="times-circle"></clr-icon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="clr-col padding-top-8">
|
||||
<div class="form-group padding-left-80">
|
||||
<label for="expires">{{'CVE_WHITELIST.EXPIRES_AT'|translate}}</label>
|
||||
<div class="underline">
|
||||
<input #dateInput placeholder="{{'CVE_WHITELIST.NEVER_EXPIRES'|translate}}" readonly
|
||||
type="date" [(clrDate)]="expiresDate" newFormLayout="true">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group padding-left-80">
|
||||
<clr-checkbox-wrapper>
|
||||
<input [checked]="neverExpires" [(ngModel)]="neverExpires" type="checkbox" clrCheckbox
|
||||
name="neverExpires" id="neverExpires"/>
|
||||
<label>
|
||||
{{'CVE_WHITELIST.NEVER_EXPIRES'|translate}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<div>
|
||||
<button type="button" id="config_system_save" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE'
|
||||
<button type="button" id="config_system_save" class="btn btn-primary" (click)="save()"
|
||||
[disabled]="(!isValid() || !hasChanges()) && (!hasWhitelistChanged) || inProgress">{{'BUTTON.SAVE'
|
||||
| translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL'
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()"
|
||||
[disabled]="(!isValid() || !hasChanges()) && (!hasWhitelistChanged) || inProgress">{{'BUTTON.CANCEL'
|
||||
| translate}}</button>
|
||||
</div>
|
||||
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
|
@ -1,12 +1,75 @@
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.create-tooltip {
|
||||
top: -1;
|
||||
top: -1;
|
||||
}
|
||||
|
||||
.read-tooltip {
|
||||
top: -7px;
|
||||
top: -7px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.margin-top-4 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.whitelist-window {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
padding: 12px;
|
||||
height: 224px;
|
||||
width: 222px;
|
||||
color: #0079bb;
|
||||
overflow-y: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
li {
|
||||
height: 24px;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
.width-70per {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.none {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.underline {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.color-0079bb {
|
||||
color: #0079bb;
|
||||
}
|
||||
|
||||
.padding-top-8 {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.padding-left-80 {
|
||||
padding-left: 80px;
|
||||
}
|
||||
|
||||
.add-modal {
|
||||
position: absolute;
|
||||
padding: 0 8px;
|
||||
background-color: rgb(238, 238, 238);
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -1,20 +1,34 @@
|
||||
import { Component, Input, OnInit, Output, EventEmitter, ViewChild, Inject, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { Configuration, StringValueItem } from '../config';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../../service.config';
|
||||
import { clone, isEmpty, getChanges } from '../../utils';
|
||||
import { ErrorHandler } from '../../error-handler/index';
|
||||
import { ConfirmationMessage } from '../../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationDialogComponent } from '../../confirmation-dialog/confirmation-dialog.component';
|
||||
import { ConfirmationState, ConfirmationTargets } from '../../shared/shared.const';
|
||||
import { ConfirmationAcknowledgement } from '../../confirmation-dialog/confirmation-state-message';
|
||||
import {
|
||||
ConfigurationService
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ViewChild,
|
||||
Inject,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
ElementRef
|
||||
} from '@angular/core';
|
||||
import {NgForm} from '@angular/forms';
|
||||
import {Configuration, StringValueItem} from '../config';
|
||||
import {SERVICE_CONFIG, IServiceConfig} from '../../service.config';
|
||||
import {clone, isEmpty, getChanges, compareValue} from '../../utils';
|
||||
import {ErrorHandler} from '../../error-handler/index';
|
||||
import {ConfirmationMessage} from '../../confirmation-dialog/confirmation-message';
|
||||
import {ConfirmationDialogComponent} from '../../confirmation-dialog/confirmation-dialog.component';
|
||||
import {ConfirmationState, ConfirmationTargets} from '../../shared/shared.const';
|
||||
import {ConfirmationAcknowledgement} from '../../confirmation-dialog/confirmation-state-message';
|
||||
import {
|
||||
ConfigurationService, SystemCVEWhitelist, SystemInfo, SystemInfoService, VulnerabilityItem
|
||||
} from '../../service/index';
|
||||
import { from } from 'rxjs';
|
||||
import {forkJoin} from "rxjs";
|
||||
|
||||
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
|
||||
const ONE_HOUR_MINUTES: number = 60;
|
||||
const ONE_DAY_MINUTES: number = 24 * ONE_HOUR_MINUTES;
|
||||
const ONE_THOUSAND: number = 1000;
|
||||
|
||||
@Component({
|
||||
selector: 'system-settings',
|
||||
templateUrl: './system-settings.component.html',
|
||||
@ -26,6 +40,11 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
private originalConfig: Configuration;
|
||||
downloadLink: string;
|
||||
robotTokenExpiration: string;
|
||||
systemWhitelist: SystemCVEWhitelist;
|
||||
systemWhitelistOrigin: SystemCVEWhitelist;
|
||||
cveIds: string;
|
||||
showAddModal: boolean = false;
|
||||
systemInfo: SystemInfo;
|
||||
@Output() configChange: EventEmitter<Configuration> = new EventEmitter<Configuration>();
|
||||
@Output() readOnlyChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
@Output() reloadSystemConfig: EventEmitter<any> = new EventEmitter<any>();
|
||||
@ -34,6 +53,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
get systemSettings(): Configuration {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
set systemSettings(cfg: Configuration) {
|
||||
this.config = cfg;
|
||||
this.configChange.emit(this.config);
|
||||
@ -46,6 +66,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
|
||||
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
|
||||
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
|
||||
@ViewChild('dateInput') dateInput: ElementRef;
|
||||
|
||||
get editable(): boolean {
|
||||
return this.systemSettings &&
|
||||
@ -85,7 +106,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
let changes = {};
|
||||
for (let prop in allChanges) {
|
||||
if (prop === 'token_expiration' || prop === 'read_only' || prop === 'project_creation_restriction'
|
||||
|| prop === 'robot_token_duration') {
|
||||
|| prop === 'robot_token_duration') {
|
||||
changes[prop] = allChanges[prop];
|
||||
}
|
||||
}
|
||||
@ -112,11 +133,18 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
*/
|
||||
public save(): void {
|
||||
let changes = this.getChanges();
|
||||
if (!isEmpty(changes)) {
|
||||
if (!isEmpty(changes) || !compareValue(this.systemWhitelistOrigin, this.systemWhitelist)) {
|
||||
this.onGoing = true;
|
||||
this.configService.saveConfigurations(changes)
|
||||
.subscribe(response => {
|
||||
this.onGoing = false;
|
||||
let observables = [];
|
||||
if (!isEmpty(changes)) {
|
||||
observables.push(this.configService.saveConfigurations(changes));
|
||||
}
|
||||
if (!compareValue(this.systemWhitelistOrigin, this.systemWhitelist)) {
|
||||
observables.push(this.systemInfoService.updateSystemWhitelist(this.systemWhitelist));
|
||||
}
|
||||
forkJoin(observables).subscribe(result => {
|
||||
this.onGoing = false;
|
||||
if (!isEmpty(changes)) {
|
||||
// API should return the updated configurations here
|
||||
// Unfortunately API does not do that
|
||||
// To refresh the view, we can clone the original data copy
|
||||
@ -128,12 +156,15 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
}
|
||||
|
||||
this.reloadSystemConfig.emit();
|
||||
this.errorHandler.info('CONFIG.SAVE_SUCCESS');
|
||||
}
|
||||
, error => {
|
||||
this.onGoing = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
if (!compareValue(this.systemWhitelistOrigin, this.systemWhitelist)) {
|
||||
this.systemWhitelistOrigin = clone(this.systemWhitelist);
|
||||
}
|
||||
this.errorHandler.info('CONFIG.SAVE_SUCCESS');
|
||||
}, error => {
|
||||
this.onGoing = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
} else {
|
||||
// Inprop situation, should not come here
|
||||
console.error('Save abort because nothing changed');
|
||||
@ -175,6 +206,9 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
let changes = this.getChanges();
|
||||
this.reset(changes);
|
||||
this.initRobotToken();
|
||||
if (!compareValue(this.systemWhitelistOrigin, this.systemWhitelist)) {
|
||||
this.systemWhitelist = clone(this.systemWhitelistOrigin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,7 +225,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
*/
|
||||
public cancel(): void {
|
||||
let changes = this.getChanges();
|
||||
if (!isEmpty(changes)) {
|
||||
if (!isEmpty(changes) || !compareValue(this.systemWhitelistOrigin, this.systemWhitelist)) {
|
||||
let msg = new ConfirmationMessage(
|
||||
'CONFIG.CONFIRM_TITLE',
|
||||
'CONFIG.CONFIRM_SUMMARY',
|
||||
@ -207,23 +241,59 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
}
|
||||
|
||||
constructor(@Inject(SERVICE_CONFIG) private configInfo: IServiceConfig,
|
||||
private configService: ConfigurationService,
|
||||
private errorHandler: ErrorHandler) {
|
||||
private configService: ConfigurationService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private systemInfoService: SystemInfoService) {
|
||||
if (this.configInfo && this.configInfo.systemInfoEndpoint) {
|
||||
this.downloadLink = this.configInfo.systemInfoEndpoint + "/getcert";
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initRobotToken();
|
||||
this.getSystemWhitelist();
|
||||
this.getSystemInfo();
|
||||
}
|
||||
|
||||
private initRobotToken (): void {
|
||||
getSystemInfo() {
|
||||
this.systemInfoService.getSystemInfo()
|
||||
.subscribe(systemInfo => this.systemInfo = systemInfo
|
||||
, error => this.errorHandler.error(error));
|
||||
}
|
||||
|
||||
get withClair(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_clair : false;
|
||||
}
|
||||
|
||||
getSystemWhitelist() {
|
||||
this.onGoing = true;
|
||||
this.systemInfoService.getSystemWhitelist()
|
||||
.subscribe((systemWhitelist) => {
|
||||
this.onGoing = false;
|
||||
if (!systemWhitelist.items) {
|
||||
systemWhitelist.items = [];
|
||||
}
|
||||
if (!systemWhitelist.expires_at) {
|
||||
systemWhitelist.expires_at = null;
|
||||
}
|
||||
this.systemWhitelist = systemWhitelist;
|
||||
this.systemWhitelistOrigin = clone(systemWhitelist);
|
||||
}, error => {
|
||||
this.onGoing = false;
|
||||
console.error('An error occurred during getting systemWhitelist');
|
||||
// this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private initRobotToken(): void {
|
||||
if (this.config &&
|
||||
this.config.robot_token_duration ) {
|
||||
this.config.robot_token_duration) {
|
||||
let robotExpiration = this.config.robot_token_duration.value;
|
||||
this.robotTokenExpiration = Math.floor(robotExpiration / ONE_DAY_MINUTES) + '';
|
||||
}
|
||||
}
|
||||
|
||||
changeToken(v: string) {
|
||||
if (!v || v === "") {
|
||||
return;
|
||||
@ -235,5 +305,79 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
this.config.robot_token_duration.value = +v * ONE_DAY_MINUTES;
|
||||
}
|
||||
|
||||
deleteItem(index: number) {
|
||||
this.systemWhitelist.items.splice(index, 1);
|
||||
}
|
||||
|
||||
addToSystemWhitelist() {
|
||||
// remove duplication and add to systemWhitelist
|
||||
let map = {};
|
||||
this.systemWhitelist.items.forEach(item => {
|
||||
map[item.cve_id] = true;
|
||||
});
|
||||
this.cveIds.split(/[\n,]+/).forEach(id => {
|
||||
let cveObj: any = {};
|
||||
cveObj.cve_id = id.trim();
|
||||
if (!map[cveObj.cve_id]) {
|
||||
map[cveObj.cve_id] = true;
|
||||
this.systemWhitelist.items.push(cveObj);
|
||||
}
|
||||
});
|
||||
// clear modal and close modal
|
||||
this.cveIds = null;
|
||||
this.showAddModal = false;
|
||||
}
|
||||
|
||||
get hasWhitelistChanged(): boolean {
|
||||
return !compareValue(this.systemWhitelistOrigin, this.systemWhitelist);
|
||||
}
|
||||
|
||||
isDisabled(): boolean {
|
||||
if (this.cveIds) {
|
||||
let arr = this.cveIds.split(/[\n,]+/);
|
||||
let flag = false;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let id = arr[i].trim();
|
||||
if (!/^CVE-[\d]+-[\d]+$/.test(id)) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get expiresDate() {
|
||||
if (this.systemWhitelist && this.systemWhitelist.expires_at) {
|
||||
return new Date(this.systemWhitelist.expires_at * ONE_THOUSAND);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set expiresDate(date) {
|
||||
if (this.systemWhitelist && date) {
|
||||
this.systemWhitelist.expires_at = Math.floor(date.getTime() / ONE_THOUSAND);
|
||||
}
|
||||
}
|
||||
|
||||
get neverExpires(): boolean {
|
||||
return !(this.systemWhitelist && this.systemWhitelist.expires_at);
|
||||
}
|
||||
|
||||
set neverExpires(flag) {
|
||||
if (flag) {
|
||||
this.systemWhitelist.expires_at = null;
|
||||
this.systemInfoService.resetDateInput(this.dateInput);
|
||||
} else {
|
||||
this.systemWhitelist.expires_at = Math.floor(new Date().getTime() / ONE_THOUSAND);
|
||||
}
|
||||
}
|
||||
|
||||
get hasExpired(): boolean {
|
||||
if (this.systemWhitelistOrigin && this.systemWhitelistOrigin.expires_at) {
|
||||
return new Date().getTime() > this.systemWhitelistOrigin.expires_at * ONE_THOUSAND;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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: "" });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,68 +1,174 @@
|
||||
<form #projectPolicyForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="projectPolicyForm">{{ 'PROJECT_CONFIG.REGISTRY' | translate }}</label>
|
||||
<div class="form-content" id="clr-wrapper-public">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="clr-checkbox-wrapper-public" clrCheckbox [(ngModel)]="projectPolicy.Public" name="public"
|
||||
[disabled]="!hasChangeConfigRole" />
|
||||
<label for="clr-checkbox-wrapper-public">{{ 'PROJECT_CONFIG.PUBLIC_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="projectPolicyForm">{{ 'PROJECT_CONFIG.REGISTRY' | translate }}</label>
|
||||
<div class="form-content" id="clr-wrapper-public">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="clr-checkbox-wrapper-public" clrCheckbox
|
||||
[(ngModel)]="projectPolicy.Public" name="public"
|
||||
[disabled]="!hasChangeConfigRole"/>
|
||||
<label for="clr-checkbox-wrapper-public">{{ 'PROJECT_CONFIG.PUBLIC_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<div>
|
||||
<label> {{ 'PROJECT_CONFIG.PUBLIC_POLICY' | translate }} </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="withNotary || withClair">
|
||||
<label for="projectPolicyForm">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</label>
|
||||
<div class="form-content">
|
||||
<div *ngIf="withNotary">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ContentTrust" name="content-trust" [disabled]="!hasChangeConfigRole" />
|
||||
<label>{{ 'PROJECT_CONFIG.CONTENT_TRUST_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<div class="chk-explain"><label> {{ 'PROJECT_CONFIG.CONTENT_TRUST_POLCIY' | translate }} </label></div>
|
||||
</div>
|
||||
<div *ngIf="withClair" id="prevent-vulenrability-image">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.PreventVulImg" name="prevent-vulenrability-image-input"
|
||||
[disabled]="!hasChangeConfigRole" />
|
||||
<label>{{ 'PROJECT_CONFIG.PREVENT_VULNERABLE_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<div class="chk-explain">
|
||||
<label>
|
||||
<div id="severity-blk">
|
||||
<div>{{ 'PROJECT_CONFIG.PREVENT_VULNERABLE_1' | translate }}</div>
|
||||
<div class="select">
|
||||
<select id="severity" name="severity" [(ngModel)]="projectPolicy.PreventVulImgSeverity" [disabled]="!projectPolicy.PreventVulImg">
|
||||
<option *ngFor='let s of severityOptions' [ngValue]="s.severity">{{ s.severityLevel | translate }}</option>
|
||||
</select>
|
||||
<div>
|
||||
<label> {{ 'PROJECT_CONFIG.PUBLIC_POLICY' | translate }} </label>
|
||||
</div>
|
||||
<div> {{ 'PROJECT_CONFIG.PREVENT_VULNERABLE_2' | translate }} </div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="withClair">
|
||||
<label for="projectPolicyForm">{{ 'PROJECT_CONFIG.SCAN' | translate }}</label>
|
||||
<div class="form-content">
|
||||
<clr-checkbox-wrapper id="scan-image-on-push-wrapper">
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ScanImgOnPush" [disabled]="!hasChangeConfigRole"
|
||||
name="scan-image-on-push" />
|
||||
<label>{{ 'PROJECT_CONFIG.AUTOSCAN_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<div class="chk-explain"><label> {{ 'PROJECT_CONFIG.AUTOSCAN_POLICY' | translate }}</label></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges() || !hasChangeConfigRole">{{'BUTTON.SAVE'
|
||||
| translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges() || !hasChangeConfigRole">{{'BUTTON.CANCEL'
|
||||
| translate}}</button>
|
||||
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
|
||||
</section>
|
||||
<div class="form-group" *ngIf="withNotary || withClair">
|
||||
<label for="projectPolicyForm">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</label>
|
||||
<div class="form-content">
|
||||
<div *ngIf="withNotary">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ContentTrust" name="content-trust"
|
||||
[disabled]="!hasChangeConfigRole"/>
|
||||
<label>{{ 'PROJECT_CONFIG.CONTENT_TRUST_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<div class="chk-explain"><label> {{ 'PROJECT_CONFIG.CONTENT_TRUST_POLCIY' | translate }} </label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="withClair" id="prevent-vulenrability-image">
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.PreventVulImg"
|
||||
name="prevent-vulenrability-image-input"
|
||||
[disabled]="!hasChangeConfigRole"/>
|
||||
<label>{{ 'PROJECT_CONFIG.PREVENT_VULNERABLE_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<div class="chk-explain">
|
||||
<label>
|
||||
<div id="severity-blk">
|
||||
<div>{{ 'PROJECT_CONFIG.PREVENT_VULNERABLE_1' | translate }}</div>
|
||||
<div class="select">
|
||||
<select id="severity" name="severity"
|
||||
[(ngModel)]="projectPolicy.PreventVulImgSeverity"
|
||||
[disabled]="!projectPolicy.PreventVulImg">
|
||||
<option *ngFor='let s of severityOptions'
|
||||
[ngValue]="s.severity">{{ s.severityLevel | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div> {{ 'PROJECT_CONFIG.PREVENT_VULNERABLE_2' | translate }} </div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="withClair">
|
||||
<label for="projectPolicyForm">{{ 'PROJECT_CONFIG.SCAN' | translate }}</label>
|
||||
<div class="form-content">
|
||||
<clr-checkbox-wrapper id="scan-image-on-push-wrapper">
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ScanImgOnPush"
|
||||
[disabled]="!hasChangeConfigRole"
|
||||
name="scan-image-on-push"/>
|
||||
<label>{{ 'PROJECT_CONFIG.AUTOSCAN_TOGGLE' | translate }}</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<div class="chk-explain"><label> {{ 'PROJECT_CONFIG.AUTOSCAN_POLICY' | translate }}</label></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="withClair">
|
||||
<label for="systemWhitelist">{{'CVE_WHITELIST.CVE_WHITELIST'|translate}}</label>
|
||||
<div class="form-content w-100">
|
||||
<div>
|
||||
<div>
|
||||
<span>{{'CVE_WHITELIST.PRO_WHITELIST_EXPLAIN'|translate}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{'CVE_WHITELIST.PRO_OR_SYS'|translate}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{'CVE_WHITELIST.MERGE_INTO'|translate}}</span>
|
||||
</div>
|
||||
<div *ngIf="hasExpired">
|
||||
<span *ngIf="isUseSystemWhitelist()"
|
||||
class="label label-warning">{{'CVE_WHITELIST.WARNING_SYS'|translate}}</span>
|
||||
<span *ngIf="!isUseSystemWhitelist()"
|
||||
class="label label-warning">{{'CVE_WHITELIST.WARNING_PRO'|translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<clr-radio-container clrInline>
|
||||
<clr-radio-wrapper>
|
||||
<input type="radio" clrRadio name="systemWhitelistOrProjectWhitelist" required value="true"
|
||||
[(ngModel)]="systemWhitelistOrProjectWhitelist"/>
|
||||
<label>{{'CVE_WHITELIST.SYS_WHITELIST'|translate}}</label>
|
||||
</clr-radio-wrapper>
|
||||
<clr-radio-wrapper>
|
||||
<input type="radio" clrRadio name="systemWhitelistOrProjectWhitelist" required value="false"
|
||||
[(ngModel)]="systemWhitelistOrProjectWhitelist"/>
|
||||
<label>{{'CVE_WHITELIST.PRO_WHITELIST'|translate}}</label>
|
||||
</clr-radio-wrapper>
|
||||
</clr-radio-container>
|
||||
<div class="clr-row width-70per">
|
||||
<div class="clr-col position-relative">
|
||||
<div>
|
||||
<button [disabled]="isUseSystemWhitelist()"
|
||||
(click)="showAddModal=!showAddModal"
|
||||
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
|
||||
<button [disabled]="isUseSystemWhitelist()" (click)="addSystem()"
|
||||
class="btn btn-link ml-1">{{'CVE_WHITELIST.ADD_SYSTEM'|translate}}</button>
|
||||
</div>
|
||||
<div class="add-modal" *ngIf="showAddModal && !isUseSystemWhitelist()">
|
||||
<div>
|
||||
<clr-textarea-container>
|
||||
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
|
||||
<textarea class="w-100" clrTextarea [(ngModel)]="cveIds" name="cveIds"></textarea>
|
||||
<clr-control-helper>{{'CVE_WHITELIST.HELP'|translate}}</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
</div>
|
||||
<div>
|
||||
<button [disabled]="isDisabled()" (click)="addToProjectWhitelist()"
|
||||
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="whitelist-window" *ngIf="isUseSystemWhitelist()">
|
||||
<li *ngIf="systemWhitelist?.items?.length<1"
|
||||
class="none">{{'CVE_WHITELIST.NONE'|translate}}</li>
|
||||
<li *ngFor="let item of systemWhitelist?.items">{{item.cve_id}}</li>
|
||||
</ul>
|
||||
<ul class="whitelist-window" *ngIf="!isUseSystemWhitelist()">
|
||||
<li class="none"
|
||||
*ngIf="projectWhitelist?.items?.length<1">{{'CVE_WHITELIST.NONE'|translate}}</li>
|
||||
<li *ngFor="let item of projectWhitelist?.items;let i = index;">{{item.cve_id}}
|
||||
<clr-icon (click)="deleteItem(i)" class="float-lg-right margin-top-4"
|
||||
shape="times-circle"></clr-icon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="clr-col padding-top-8">
|
||||
<div class="form-group padding-left-80">
|
||||
<label for="expires">{{'CVE_WHITELIST.EXPIRES_AT'|translate}}</label>
|
||||
<div class="underline">
|
||||
<input #dateSystemInput readonly type="date" [(clrDate)]="systemExpiresDate">
|
||||
<input *ngIf="!isUseSystemWhitelist()" #dateInput
|
||||
placeholder="{{'CVE_WHITELIST.NEVER_EXPIRES'|translate}}" readonly type="date"
|
||||
[(clrDate)]="expiresDate" newFormLayout="true">
|
||||
<input *ngIf="isUseSystemWhitelist()"
|
||||
placeholder="{{'CVE_WHITELIST.NEVER_EXPIRES'|translate}}" readonly type="text"
|
||||
value="{{systemExpiresDateString}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group padding-left-80">
|
||||
<clr-checkbox-wrapper>
|
||||
<input [disabled]="isUseSystemWhitelist()" [checked]="neverExpires"
|
||||
[(ngModel)]="neverExpires" type="checkbox" clrCheckbox name="neverExpires"
|
||||
id="neverExpires"/>
|
||||
<label>
|
||||
{{'CVE_WHITELIST.NEVER_EXPIRES'|translate}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()"
|
||||
[disabled]="(!isValid() || !hasChanges() || !hasChangeConfigRole) && !hasWhitelistChanged">{{'BUTTON.SAVE'
|
||||
| translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()"
|
||||
[disabled]="(!isValid() || !hasChanges() || !hasChangeConfigRole) && !hasWhitelistChanged">{{'BUTTON.CANCEL'
|
||||
| translate}}</button>
|
||||
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
|
||||
</section>
|
||||
</form>
|
@ -1,8 +1,63 @@
|
||||
#severity-blk div
|
||||
{
|
||||
display: inline-block;
|
||||
#severity-blk div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 120px;
|
||||
}
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.whitelist-window {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
padding: 12px;
|
||||
height: 224px;
|
||||
width: 222px;
|
||||
color: #0079bb;
|
||||
overflow-y: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
li {
|
||||
height: 24px;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
.width-70per {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.none {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.underline {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.color-0079bb {
|
||||
color: #0079bb;
|
||||
}
|
||||
|
||||
.padding-top-8 {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.padding-left-80 {
|
||||
padding-left: 80px;
|
||||
}
|
||||
|
||||
.add-modal {
|
||||
position: absolute;
|
||||
padding: 0 8px;
|
||||
background-color: rgb(238, 238, 238);
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { ProjectPolicyConfigComponent } from './project-policy-config.component'
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ProjectService, ProjectDefaultService} from '../service/project.service';
|
||||
import { SERVICE_CONFIG, IServiceConfig} from '../service.config';
|
||||
import { SystemInfo } from '../service/interface';
|
||||
import {SystemCVEWhitelist, SystemInfo} from '../service/interface';
|
||||
import { Project } from './project';
|
||||
import { UserPermissionService, UserPermissionDefaultService } from '../service/permission.service';
|
||||
import { USERSTATICPERMISSION } from '../service/permission-static';
|
||||
@ -83,8 +83,12 @@ describe('ProjectPolicyConfigComponent', () => {
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
let mockSystemWhitelist: SystemCVEWhitelist = {
|
||||
"expires_at": 1561996800,
|
||||
"id": 1,
|
||||
"items": [],
|
||||
"project_id": 0
|
||||
};
|
||||
let component: ProjectPolicyConfigComponent;
|
||||
let fixture: ComponentFixture<ProjectPolicyConfigComponent>;
|
||||
|
||||
@ -122,6 +126,7 @@ describe('ProjectPolicyConfigComponent', () => {
|
||||
projectPolicyService = fixture.debugElement.injector.get(ProjectService);
|
||||
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(of(mockSystemInfo[0]));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemWhitelist').and.returnValue(of(mockSystemWhitelist));
|
||||
spyProjectPolicies = spyOn(projectPolicyService, 'getProject').and.returnValues(of(mockProjectPolicies[0]));
|
||||
|
||||
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
|
||||
|
@ -1,176 +1,370 @@
|
||||
import { Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
|
||||
|
||||
import { compareValue, clone } from '../utils';
|
||||
import { ProjectService } from '../service/project.service';
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { State } from '../service/interface';
|
||||
import {compareValue, clone} from '../utils';
|
||||
import {ProjectService} from '../service/project.service';
|
||||
import {ErrorHandler} from '../error-handler/error-handler';
|
||||
import {State, SystemCVEWhitelist} from '../service/interface';
|
||||
|
||||
import { ConfirmationState, ConfirmationTargets } from '../shared/shared.const';
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {ConfirmationState, ConfirmationTargets} from '../shared/shared.const';
|
||||
import {ConfirmationMessage} from '../confirmation-dialog/confirmation-message';
|
||||
import {ConfirmationDialogComponent} from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import {ConfirmationAcknowledgement} from '../confirmation-dialog/confirmation-state-message';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
|
||||
import { Project } from './project';
|
||||
import {Project} from './project';
|
||||
import {SystemInfo, SystemInfoService} from '../service/index';
|
||||
import { UserPermissionService } from '../service/permission.service';
|
||||
import { USERSTATICPERMISSION } from '../service/permission-static';
|
||||
import {UserPermissionService} from '../service/permission.service';
|
||||
import {USERSTATICPERMISSION} from '../service/permission-static';
|
||||
|
||||
|
||||
const ONE_THOUSAND: number = 1000;
|
||||
const LOW: string = 'low';
|
||||
|
||||
export class ProjectPolicy {
|
||||
Public: boolean;
|
||||
ContentTrust: boolean;
|
||||
PreventVulImg: boolean;
|
||||
PreventVulImgSeverity: string;
|
||||
ScanImgOnPush: boolean;
|
||||
Public: boolean;
|
||||
ContentTrust: boolean;
|
||||
PreventVulImg: boolean;
|
||||
PreventVulImgSeverity: string;
|
||||
ScanImgOnPush: boolean;
|
||||
|
||||
constructor() {
|
||||
this.Public = false;
|
||||
this.ContentTrust = false;
|
||||
this.PreventVulImg = false;
|
||||
this.PreventVulImgSeverity = 'low';
|
||||
this.ScanImgOnPush = false;
|
||||
}
|
||||
constructor() {
|
||||
this.Public = false;
|
||||
this.ContentTrust = false;
|
||||
this.PreventVulImg = false;
|
||||
this.PreventVulImgSeverity = LOW;
|
||||
this.ScanImgOnPush = false;
|
||||
}
|
||||
|
||||
initByProject(pro: Project) {
|
||||
this.Public = pro.metadata.public === 'true' ? true : false;
|
||||
this.ContentTrust = pro.metadata.enable_content_trust === 'true' ? true : false;
|
||||
this.PreventVulImg = pro.metadata.prevent_vul === 'true' ? true : false;
|
||||
if (pro.metadata.severity) { this.PreventVulImgSeverity = pro.metadata.severity; }
|
||||
this.ScanImgOnPush = pro.metadata.auto_scan === 'true' ? true : false;
|
||||
}
|
||||
initByProject(pro: Project) {
|
||||
this.Public = pro.metadata.public === 'true' ? true : false;
|
||||
this.ContentTrust = pro.metadata.enable_content_trust === 'true' ? true : false;
|
||||
this.PreventVulImg = pro.metadata.prevent_vul === 'true' ? true : false;
|
||||
if (pro.metadata.severity) {
|
||||
this.PreventVulImgSeverity = pro.metadata.severity;
|
||||
}
|
||||
this.ScanImgOnPush = pro.metadata.auto_scan === 'true' ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-project-policy-config',
|
||||
templateUrl: './project-policy-config.component.html',
|
||||
styleUrls: ['./project-policy-config.component.scss']
|
||||
selector: 'hbr-project-policy-config',
|
||||
templateUrl: './project-policy-config.component.html',
|
||||
styleUrls: ['./project-policy-config.component.scss']
|
||||
})
|
||||
export class ProjectPolicyConfigComponent implements OnInit {
|
||||
onGoing = false;
|
||||
@Input() projectId: number;
|
||||
@Input() projectName = 'unknown';
|
||||
onGoing = false;
|
||||
@Input() projectId: number;
|
||||
@Input() projectName = 'unknown';
|
||||
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
|
||||
@ViewChild('cfgConfirmationDialog') confirmationDlg: ConfirmationDialogComponent;
|
||||
@ViewChild('cfgConfirmationDialog') confirmationDlg: ConfirmationDialogComponent;
|
||||
@ViewChild('dateInput') dateInput: ElementRef;
|
||||
@ViewChild('dateSystemInput') dateSystemInput: ElementRef;
|
||||
|
||||
systemInfo: SystemInfo;
|
||||
orgProjectPolicy = new ProjectPolicy();
|
||||
projectPolicy = new ProjectPolicy();
|
||||
hasChangeConfigRole: boolean;
|
||||
severityOptions = [
|
||||
{severity: 'high', severityLevel: 'VULNERABILITY.SEVERITY.HIGH'},
|
||||
{severity: 'medium', severityLevel: 'VULNERABILITY.SEVERITY.MEDIUM'},
|
||||
{severity: 'low', severityLevel: 'VULNERABILITY.SEVERITY.LOW'},
|
||||
{severity: 'negligible', severityLevel: 'VULNERABILITY.SEVERITY.NEGLIGIBLE'},
|
||||
];
|
||||
systemInfo: SystemInfo;
|
||||
orgProjectPolicy = new ProjectPolicy();
|
||||
projectPolicy = new ProjectPolicy();
|
||||
hasChangeConfigRole: boolean;
|
||||
severityOptions = [
|
||||
{severity: 'high', severityLevel: 'VULNERABILITY.SEVERITY.HIGH'},
|
||||
{severity: 'medium', severityLevel: 'VULNERABILITY.SEVERITY.MEDIUM'},
|
||||
{severity: 'low', severityLevel: 'VULNERABILITY.SEVERITY.LOW'},
|
||||
{severity: 'negligible', severityLevel: 'VULNERABILITY.SEVERITY.NEGLIGIBLE'},
|
||||
];
|
||||
userSystemWhitelist: boolean = true;
|
||||
showAddModal: boolean = false;
|
||||
systemWhitelist: SystemCVEWhitelist;
|
||||
cveIds: string;
|
||||
systemExpiresDate: Date;
|
||||
systemExpiresDateString: string;
|
||||
userProjectWhitelist = false;
|
||||
systemWhitelistOrProjectWhitelist: string;
|
||||
systemWhitelistOrProjectWhitelistOrigin: string;
|
||||
projectWhitelist;
|
||||
projectWhitelistOrigin;
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private translate: TranslateService,
|
||||
private projectService: ProjectService,
|
||||
private systemInfoService: SystemInfoService,
|
||||
private userPermission: UserPermissionService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// assert if project id exist
|
||||
if (!this.projectId) {
|
||||
this.errorHandler.error('Project ID cannot be unset.');
|
||||
return;
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private translate: TranslateService,
|
||||
private projectService: ProjectService,
|
||||
private systemInfoService: SystemInfoService,
|
||||
private userPermission: UserPermissionService,
|
||||
) {
|
||||
}
|
||||
|
||||
// get system info
|
||||
this.systemInfoService.getSystemInfo()
|
||||
.subscribe(systemInfo => this.systemInfo = systemInfo
|
||||
, error => this.errorHandler.error(error));
|
||||
ngOnInit(): void {
|
||||
// assert if project id exist
|
||||
if (!this.projectId) {
|
||||
this.errorHandler.error('Project ID cannot be unset.');
|
||||
return;
|
||||
}
|
||||
// get system info
|
||||
this.systemInfoService.getSystemInfo()
|
||||
.subscribe(systemInfo => {
|
||||
this.systemInfo = systemInfo;
|
||||
setTimeout(() => {
|
||||
this.dateSystemInput.nativeElement.parentNode.setAttribute("hidden", "hidden");
|
||||
}, 100);
|
||||
} , error => this.errorHandler.error(error));
|
||||
|
||||
// retrive project level policy data
|
||||
this.retrieve();
|
||||
this.getPermission();
|
||||
}
|
||||
private getPermission(): void {
|
||||
this.userPermission.getPermission(this.projectId,
|
||||
USERSTATICPERMISSION.CONFIGURATION.KEY, USERSTATICPERMISSION.CONFIGURATION.VALUE.UPDATE).subscribe(permissins => {
|
||||
this.hasChangeConfigRole = permissins as boolean;
|
||||
});
|
||||
}
|
||||
public get withNotary(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_notary : false;
|
||||
}
|
||||
|
||||
public get withClair(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_clair : false;
|
||||
}
|
||||
|
||||
retrieve(state?: State): any {
|
||||
this.projectService.getProject(this.projectId)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.orgProjectPolicy.initByProject(response);
|
||||
this.projectPolicy.initByProject(response);
|
||||
}, error => this.errorHandler.error(error));
|
||||
}
|
||||
|
||||
updateProjectPolicy(projectId: string|number, pp: ProjectPolicy) {
|
||||
this.projectService.updateProjectPolicy(projectId, pp);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
isValid() {
|
||||
let flag = false;
|
||||
if (!this.projectPolicy.PreventVulImg || this.severityOptions.some(x => x.severity === this.projectPolicy.PreventVulImgSeverity)) {
|
||||
flag = true;
|
||||
// retrive project level policy data
|
||||
this.retrieve();
|
||||
this.getPermission();
|
||||
this.getSystemWhitelist();
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return !compareValue(this.orgProjectPolicy, this.projectPolicy);
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.hasChanges()) {
|
||||
return;
|
||||
getSystemWhitelist() {
|
||||
this.systemInfoService.getSystemWhitelist()
|
||||
.subscribe((systemWhitelist) => {
|
||||
if (systemWhitelist) {
|
||||
this.systemWhitelist = systemWhitelist;
|
||||
if (this.systemWhitelist.expires_at) {
|
||||
this.systemExpiresDate = new Date(this.systemWhitelist.expires_at * ONE_THOUSAND);
|
||||
setTimeout( () => {
|
||||
this.systemExpiresDateString = this.dateSystemInput.nativeElement.value;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
this.onGoing = true;
|
||||
this.projectService.updateProjectPolicy(this.projectId, this.projectPolicy)
|
||||
.subscribe(() => {
|
||||
this.onGoing = false;
|
||||
|
||||
this.translate.get('CONFIG.SAVE_SUCCESS').subscribe((res: string) => {
|
||||
this.errorHandler.info(res);
|
||||
});
|
||||
this.refresh();
|
||||
}, error => {
|
||||
this.onGoing = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
let msg = new ConfirmationMessage(
|
||||
'CONFIG.CONFIRM_TITLE',
|
||||
'CONFIG.CONFIRM_SUMMARY',
|
||||
'',
|
||||
{},
|
||||
ConfirmationTargets.CONFIG
|
||||
);
|
||||
this.confirmationDlg.open(msg);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.projectPolicy = clone(this.orgProjectPolicy);
|
||||
}
|
||||
|
||||
confirmCancel(ack: ConfirmationAcknowledgement): void {
|
||||
if (ack && ack.source === ConfirmationTargets.CONFIG &&
|
||||
ack.state === ConfirmationState.CONFIRMED) {
|
||||
this.reset();
|
||||
private getPermission(): void {
|
||||
this.userPermission.getPermission(this.projectId,
|
||||
USERSTATICPERMISSION.CONFIGURATION.KEY, USERSTATICPERMISSION.CONFIGURATION.VALUE.UPDATE).subscribe(permissins => {
|
||||
this.hasChangeConfigRole = permissins as boolean;
|
||||
});
|
||||
}
|
||||
|
||||
public get withNotary(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_notary : false;
|
||||
}
|
||||
|
||||
public get withClair(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_clair : false;
|
||||
}
|
||||
|
||||
retrieve(state?: State): any {
|
||||
this.projectService.getProject(this.projectId)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.orgProjectPolicy.initByProject(response);
|
||||
this.projectPolicy.initByProject(response);
|
||||
// get projectWhitelist
|
||||
if (!response.cve_whitelist) {
|
||||
response.cve_whitelist = {
|
||||
items: [],
|
||||
expires_at: null
|
||||
};
|
||||
}
|
||||
if (!response.cve_whitelist['items']) {
|
||||
response.cve_whitelist['items'] = [];
|
||||
}
|
||||
if (!response.cve_whitelist['expires_at']) {
|
||||
response.cve_whitelist['expires_at'] = null;
|
||||
}
|
||||
if (response && response.cve_whitelist) {
|
||||
this.projectWhitelist = clone(response.cve_whitelist);
|
||||
this.projectWhitelistOrigin = clone(response.cve_whitelist);
|
||||
this.systemWhitelistOrProjectWhitelist = response.metadata.reuse_sys_cve_whitelist;
|
||||
this.systemWhitelistOrProjectWhitelistOrigin = response.metadata.reuse_sys_cve_whitelist;
|
||||
}
|
||||
}, error => this.errorHandler.error(error));
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
isValid() {
|
||||
let flag = false;
|
||||
if (!this.projectPolicy.PreventVulImg || this.severityOptions.some(x => x.severity === this.projectPolicy.PreventVulImgSeverity)) {
|
||||
flag = true;
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return !compareValue(this.orgProjectPolicy, this.projectPolicy);
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.hasChanges() && !this.hasWhitelistChanged) {
|
||||
return;
|
||||
}
|
||||
this.onGoing = true;
|
||||
this.projectService.updateProjectPolicy(
|
||||
this.projectId,
|
||||
this.projectPolicy,
|
||||
this.systemWhitelistOrProjectWhitelist,
|
||||
this.projectWhitelist)
|
||||
.subscribe(() => {
|
||||
this.onGoing = false;
|
||||
this.translate.get('CONFIG.SAVE_SUCCESS').subscribe((res: string) => {
|
||||
this.errorHandler.info(res);
|
||||
});
|
||||
this.refresh();
|
||||
}, error => {
|
||||
this.onGoing = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
let msg = new ConfirmationMessage(
|
||||
'CONFIG.CONFIRM_TITLE',
|
||||
'CONFIG.CONFIRM_SUMMARY',
|
||||
'',
|
||||
{},
|
||||
ConfirmationTargets.CONFIG
|
||||
);
|
||||
this.confirmationDlg.open(msg);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.projectPolicy = clone(this.orgProjectPolicy);
|
||||
}
|
||||
|
||||
confirmCancel(ack: ConfirmationAcknowledgement): void {
|
||||
if (ack && ack.source === ConfirmationTargets.CONFIG &&
|
||||
ack.state === ConfirmationState.CONFIRMED) {
|
||||
this.reset();
|
||||
if (this.hasWhitelistChanged) {
|
||||
this.projectWhitelist = clone(this.projectWhitelistOrigin);
|
||||
this.systemWhitelistOrProjectWhitelist = this.systemWhitelistOrProjectWhitelistOrigin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isUseSystemWhitelist(): boolean {
|
||||
return this.systemWhitelistOrProjectWhitelist === 'true';
|
||||
}
|
||||
|
||||
deleteItem(index: number) {
|
||||
this.projectWhitelist.items.splice(index, 1);
|
||||
}
|
||||
|
||||
addSystem() {
|
||||
this.showAddModal = false;
|
||||
if (!(this.systemWhitelist && this.systemWhitelist.items && this.systemWhitelist.items.length > 0)) {
|
||||
return;
|
||||
}
|
||||
if (this.projectWhitelist && !this.projectWhitelist.items) {
|
||||
this.projectWhitelist.items = [];
|
||||
}
|
||||
// remove duplication and add to projectWhitelist
|
||||
let map = {};
|
||||
this.projectWhitelist.items.forEach(item => {
|
||||
map[item.cve_id] = true;
|
||||
});
|
||||
this.systemWhitelist.items.forEach(item => {
|
||||
if (!map[item.cve_id]) {
|
||||
map[item.cve_id] = true;
|
||||
this.projectWhitelist.items.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addToProjectWhitelist() {
|
||||
if (this.projectWhitelist && !this.projectWhitelist.items) {
|
||||
this.projectWhitelist.items = [];
|
||||
}
|
||||
// remove duplication and add to projectWhitelist
|
||||
let map = {};
|
||||
this.projectWhitelist.items.forEach(item => {
|
||||
map[item.cve_id] = true;
|
||||
});
|
||||
this.cveIds.split(/[\n,]+/).forEach(id => {
|
||||
let cveObj: any = {};
|
||||
cveObj.cve_id = id.trim();
|
||||
if (!map[cveObj.cve_id]) {
|
||||
map[cveObj.cve_id] = true;
|
||||
this.projectWhitelist.items.push(cveObj);
|
||||
}
|
||||
});
|
||||
// clear modal and close modal
|
||||
this.cveIds = null;
|
||||
this.showAddModal = false;
|
||||
}
|
||||
|
||||
get hasWhitelistChanged(): boolean {
|
||||
return !(compareValue(this.projectWhitelist, this.projectWhitelistOrigin)
|
||||
&& this.systemWhitelistOrProjectWhitelistOrigin === this.systemWhitelistOrProjectWhitelist);
|
||||
}
|
||||
|
||||
isDisabled(): boolean {
|
||||
if (this.cveIds) {
|
||||
let arr = this.cveIds.split(/[\n,]+/);
|
||||
let flag = false;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let id = arr[i].trim();
|
||||
if (!/^CVE-[\d]+-[\d]+$/.test(id)) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get expiresDate() {
|
||||
if (this.systemWhitelistOrProjectWhitelist === 'true') {
|
||||
if (this.systemWhitelist && this.systemWhitelist.expires_at) {
|
||||
return new Date(this.systemWhitelist.expires_at * ONE_THOUSAND);
|
||||
}
|
||||
} else {
|
||||
if (this.projectWhitelist && this.projectWhitelist.expires_at) {
|
||||
return new Date(this.projectWhitelist.expires_at * ONE_THOUSAND);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set expiresDate(date) {
|
||||
if (this.systemWhitelistOrProjectWhitelist === 'false') {
|
||||
if (this.projectWhitelist && date) {
|
||||
this.projectWhitelist.expires_at = Math.floor(date.getTime() / ONE_THOUSAND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get neverExpires(): boolean {
|
||||
if (this.systemWhitelistOrProjectWhitelist === 'true') {
|
||||
if (this.systemWhitelist && this.systemWhitelist.expires_at) {
|
||||
return !(this.systemWhitelist && this.systemWhitelist.expires_at);
|
||||
}
|
||||
} else {
|
||||
if (this.projectWhitelist && this.projectWhitelist.expires_at) {
|
||||
return !(this.projectWhitelist && this.projectWhitelist.expires_at);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
set neverExpires(flag) {
|
||||
if (flag) {
|
||||
this.projectWhitelist.expires_at = null;
|
||||
this.systemInfoService.resetDateInput(this.dateInput);
|
||||
} else {
|
||||
this.projectWhitelist.expires_at = Math.floor(new Date().getTime() / ONE_THOUSAND);
|
||||
}
|
||||
}
|
||||
|
||||
get hasExpired(): boolean {
|
||||
if (this.systemWhitelistOrProjectWhitelist === 'true') {
|
||||
if (this.systemWhitelist && this.systemWhitelist.expires_at) {
|
||||
return new Date().getTime() > this.systemWhitelist.expires_at * ONE_THOUSAND;
|
||||
}
|
||||
} else {
|
||||
if (this.projectWhitelistOrigin && this.projectWhitelistOrigin.expires_at) {
|
||||
return new Date().getTime() > this.projectWhitelistOrigin.expires_at * ONE_THOUSAND;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,9 @@ export class Project {
|
||||
prevent_vul: string | boolean;
|
||||
severity: string;
|
||||
auto_scan: string | boolean;
|
||||
reuse_sys_cve_whitelist?: string;
|
||||
};
|
||||
cve_whitelist?: object;
|
||||
constructor () {
|
||||
this.metadata.public = false;
|
||||
this.metadata.enable_content_trust = false;
|
||||
|
@ -434,3 +434,15 @@ export interface HttpOptionTextInterface {
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface ProjectRootInterface {
|
||||
NAME: string;
|
||||
VALUE: number;
|
||||
LABEL: string;
|
||||
}
|
||||
export interface SystemCVEWhitelist {
|
||||
id: number;
|
||||
project_id: number;
|
||||
expires_at: number;
|
||||
items: Array<{ "cve_id": string; }>;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
|
||||
import {throwError as observableThrowError, Observable } from "rxjs";
|
||||
import { Injectable, Inject } from "@angular/core";
|
||||
import {Injectable, Inject} from "@angular/core";
|
||||
import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http";
|
||||
import { map , catchError } from "rxjs/operators";
|
||||
import { catchError } from "rxjs/operators";
|
||||
|
||||
import { SERVICE_CONFIG, IServiceConfig } from "../service.config";
|
||||
import { Project } from "../project-policy-config/project";
|
||||
@ -12,7 +12,6 @@ import {
|
||||
HTTP_GET_OPTIONS,
|
||||
buildHttpRequestOptionsWithObserveResponse
|
||||
} from "../utils";
|
||||
import { RequestQueryParams } from "./RequestQueryParams";
|
||||
|
||||
/**
|
||||
* Define the service methods to handle the Project related things.
|
||||
@ -47,7 +46,9 @@ export abstract class ProjectService {
|
||||
*/
|
||||
abstract updateProjectPolicy(
|
||||
projectId: number | string,
|
||||
projectPolicy: ProjectPolicy
|
||||
projectPolicy: ProjectPolicy,
|
||||
reuseSysCVEVWhitelist: string,
|
||||
projectWhitelist: object
|
||||
): Observable<any>;
|
||||
|
||||
/**
|
||||
@ -107,7 +108,9 @@ export class ProjectDefaultService extends ProjectService {
|
||||
|
||||
public updateProjectPolicy(
|
||||
projectId: number | string,
|
||||
projectPolicy: ProjectPolicy
|
||||
projectPolicy: ProjectPolicy,
|
||||
reuseSysCVEVWhitelist: string,
|
||||
projectWhitelist: object
|
||||
): any {
|
||||
let baseUrl: string = this.config.projectBaseEndpoint
|
||||
? this.config.projectBaseEndpoint
|
||||
@ -117,12 +120,14 @@ export class ProjectDefaultService extends ProjectService {
|
||||
`${baseUrl}/${projectId}`,
|
||||
{
|
||||
metadata: {
|
||||
public: projectPolicy.Public ? "true" : "false",
|
||||
enable_content_trust: projectPolicy.ContentTrust ? "true" : "false",
|
||||
prevent_vul: projectPolicy.PreventVulImg ? "true" : "false",
|
||||
severity: projectPolicy.PreventVulImgSeverity,
|
||||
auto_scan: projectPolicy.ScanImgOnPush ? "true" : "false"
|
||||
}
|
||||
public: projectPolicy.Public ? "true" : "false",
|
||||
enable_content_trust: projectPolicy.ContentTrust ? "true" : "false",
|
||||
prevent_vul: projectPolicy.PreventVulImg ? "true" : "false",
|
||||
severity: projectPolicy.PreventVulImgSeverity,
|
||||
auto_scan: projectPolicy.ScanImgOnPush ? "true" : "false",
|
||||
reuse_sys_cve_whitelist: reuseSysCVEVWhitelist
|
||||
},
|
||||
cve_whitelist: projectWhitelist
|
||||
},
|
||||
HTTP_JSON_OPTIONS
|
||||
)
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import {ElementRef, Inject, Injectable} from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { map, catchError } from "rxjs/operators";
|
||||
import { Observable, throwError as observableThrowError } from "rxjs";
|
||||
import { SystemInfo } from './interface';
|
||||
import {SystemCVEWhitelist, SystemInfo} from './interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { HTTP_GET_OPTIONS } from "../utils";
|
||||
import {HTTP_GET_OPTIONS, HTTP_JSON_OPTIONS} from "../utils";
|
||||
|
||||
/**
|
||||
* Get System information about current backend server.
|
||||
@ -18,6 +18,20 @@ export abstract class SystemInfoService {
|
||||
* returns
|
||||
*/
|
||||
abstract getSystemInfo(): Observable<SystemInfo>;
|
||||
/**
|
||||
* get system CEVWhitelist
|
||||
*/
|
||||
abstract getSystemWhitelist(): Observable<SystemCVEWhitelist>;
|
||||
/**
|
||||
* update systemCVEWhitelist
|
||||
* @param systemCVEWhitelist
|
||||
*/
|
||||
abstract updateSystemWhitelist(systemCVEWhitelist: SystemCVEWhitelist): Observable<any>;
|
||||
/**
|
||||
* set null to the date type input
|
||||
* @param ref
|
||||
*/
|
||||
abstract resetDateInput(ref: ElementRef);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -33,5 +47,20 @@ export class SystemInfoDefaultService extends SystemInfoService {
|
||||
.pipe(map(systemInfo => systemInfo as SystemInfo)
|
||||
, catchError(error => observableThrowError(error)));
|
||||
}
|
||||
public getSystemWhitelist(): Observable<SystemCVEWhitelist> {
|
||||
return this.http.get("/api/system/CVEWhitelist", HTTP_GET_OPTIONS)
|
||||
.pipe(map(systemCVEWhitelist => systemCVEWhitelist as SystemCVEWhitelist)
|
||||
, catchError(error => observableThrowError(error)));
|
||||
}
|
||||
public updateSystemWhitelist(systemCVEWhitelist: SystemCVEWhitelist): Observable<any> {
|
||||
return this.http.put("/api/system/CVEWhitelist", JSON.stringify(systemCVEWhitelist), HTTP_JSON_OPTIONS)
|
||||
.pipe(map(response => response)
|
||||
, catchError(error => observableThrowError(error)));
|
||||
}
|
||||
public resetDateInput(ref: ElementRef) {
|
||||
if (ref) {
|
||||
ref.nativeElement.value = null ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
171
src/portal/package-lock.json
generated
171
src/portal/package-lock.json
generated
@ -176,13 +176,15 @@
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
@ -3146,7 +3148,8 @@
|
||||
"acorn": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
|
||||
"integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA=="
|
||||
"integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-dynamic-import": {
|
||||
"version": "4.0.0",
|
||||
@ -6491,7 +6494,8 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -6515,13 +6519,15 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -6538,19 +6544,22 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -6681,7 +6690,8 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -6695,6 +6705,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -6711,6 +6722,7 @@
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -6719,13 +6731,15 @@
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz",
|
||||
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
@ -6746,6 +6760,7 @@
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -6834,7 +6849,8 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -6848,6 +6864,7 @@
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -6943,7 +6960,8 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -6985,6 +7003,7 @@
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -7006,6 +7025,7 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -7054,13 +7074,15 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
|
||||
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -10632,7 +10654,8 @@
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -10650,11 +10673,13 @@
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -10667,15 +10692,18 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -10778,7 +10806,8 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -10788,6 +10817,7 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -10800,17 +10830,20 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@ -10827,6 +10860,7 @@
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -10899,7 +10933,8 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -10909,6 +10944,7 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -10984,7 +11020,8 @@
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -11014,6 +11051,7 @@
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -11031,6 +11069,7 @@
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -11069,11 +11108,13 @@
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -17118,7 +17159,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -17139,12 +17181,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -17159,17 +17203,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -17286,7 +17333,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -17298,6 +17346,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -17312,6 +17361,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -17319,12 +17369,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@ -17343,6 +17395,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -17423,7 +17476,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -17435,6 +17489,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -17520,7 +17575,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -17556,6 +17612,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -17575,6 +17632,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -17618,12 +17676,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -18718,7 +18778,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -18739,12 +18800,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -18759,17 +18822,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -18886,7 +18952,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -18898,6 +18965,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -18912,6 +18980,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -18919,12 +18988,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@ -18943,6 +19014,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -19023,7 +19095,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -19035,6 +19108,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -19120,7 +19194,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -19156,6 +19231,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -19175,6 +19251,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -19218,12 +19295,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -2,8 +2,9 @@
|
||||
<global-message [isAppLevel]="true"></global-message>
|
||||
<navigator (showAccountSettingsModal)="openModal($event)" (showPwdChangeModal)="openModal($event)"></navigator>
|
||||
<div class="content-container">
|
||||
<div class="content-area" [class.container-override]="showSearch" [class.content-area-override]="!shouldOverrideContent"
|
||||
[class.start-content-padding]="shouldOverrideContent">
|
||||
<div class="content-area" [class.container-override]="showSearch"
|
||||
[class.content-area-override]="!shouldOverrideContent"
|
||||
[class.start-content-padding]="shouldOverrideContent">
|
||||
<global-message [isAppLevel]="false"></global-message>
|
||||
<!-- Only appear when searching -->
|
||||
<search-result></search-result>
|
||||
@ -11,56 +12,57 @@
|
||||
</div>
|
||||
<clr-vertical-nav [clrVerticalNavCollapsible]="true" *ngIf="isUserExisting">
|
||||
<div>
|
||||
<a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/projects">
|
||||
<clr-icon shape="organization" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.PROJECTS' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/logs">
|
||||
<clr-icon shape="list" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.LOGS' | translate}}
|
||||
</a>
|
||||
<clr-vertical-nav-group *ngIf="isSystemAdmin" routerLinkActive="active">
|
||||
<clr-icon shape="administrator" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}
|
||||
<a routerLink="#" hidden aria-hidden="true"></a>
|
||||
<clr-vertical-nav-group-children *clrIfExpanded="true">
|
||||
<a clrVerticalNavLink routerLink="/harbor/users" routerLinkActive="active">
|
||||
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
|
||||
</a>
|
||||
<a *ngIf='isLdapMode' clrVerticalNavLink routerLink="/harbor/groups" routerLinkActive="active">
|
||||
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLink="/harbor/registries" routerLinkActive="active">
|
||||
<clr-icon shape="block" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLink="/harbor/replications" routerLinkActive="active">
|
||||
<clr-icon shape="cloud-traffic" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLink="/harbor/configs" routerLinkActive="active">
|
||||
<clr-icon shape="cog" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}
|
||||
</a>
|
||||
</clr-vertical-nav-group-children>
|
||||
</clr-vertical-nav-group>
|
||||
<clr-vertical-nav-group *ngIf="isSystemAdmin && (withClair || hasAdminRole)" routerLinkActive="active">
|
||||
<clr-icon shape="event" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.TASKS' | translate}}
|
||||
<a routerLink="#" hidden aria-hidden="true"></a>
|
||||
<clr-vertical-nav-group-children *clrIfExpanded="true">
|
||||
<a clrVerticalNavLink *ngIf="withClair" routerLink="/harbor/vulnerability" routerLinkActive="active">
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.VULNERABILITY' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink *ngIf="hasAdminRole" routerLink="/harbor/gc" routerLinkActive="active">
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.GARBAGE_COLLECTION' | translate}}
|
||||
</a>
|
||||
</clr-vertical-nav-group-children>
|
||||
</clr-vertical-nav-group>
|
||||
<a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/projects">
|
||||
<clr-icon shape="organization" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.PROJECTS' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/logs">
|
||||
<clr-icon shape="list" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.LOGS' | translate}}
|
||||
</a>
|
||||
<clr-vertical-nav-group *ngIf="isSystemAdmin" routerLinkActive="active">
|
||||
<clr-icon shape="administrator" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}
|
||||
<a routerLink="#" hidden aria-hidden="true"></a>
|
||||
<clr-vertical-nav-group-children *clrIfExpanded="true">
|
||||
<a clrVerticalNavLink routerLink="/harbor/users" routerLinkActive="active">
|
||||
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
|
||||
</a>
|
||||
<a *ngIf='isLdapMode' clrVerticalNavLink routerLink="/harbor/groups" routerLinkActive="active">
|
||||
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLink="/harbor/registries" routerLinkActive="active">
|
||||
<clr-icon shape="block" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLink="/harbor/replications" routerLinkActive="active">
|
||||
<clr-icon shape="cloud-traffic" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}
|
||||
</a>
|
||||
</clr-vertical-nav-group-children>
|
||||
</clr-vertical-nav-group>
|
||||
<clr-vertical-nav-group *ngIf="isSystemAdmin && (withClair || hasAdminRole)" routerLinkActive="active">
|
||||
<clr-icon shape="event" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.TASKS' | translate}}
|
||||
<a routerLink="#" hidden aria-hidden="true"></a>
|
||||
<clr-vertical-nav-group-children *clrIfExpanded="true">
|
||||
<a clrVerticalNavLink *ngIf="withClair" routerLink="/harbor/vulnerability"
|
||||
routerLinkActive="active">
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.VULNERABILITY' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink *ngIf="hasAdminRole" routerLink="/harbor/gc" routerLinkActive="active">
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.GARBAGE_COLLECTION' | translate}}
|
||||
</a>
|
||||
</clr-vertical-nav-group-children>
|
||||
</clr-vertical-nav-group>
|
||||
<a *ngIf="isSystemAdmin" clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/configs">
|
||||
<clr-icon shape="cog" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="vertical-nav-footer">
|
||||
<div class="vertical-nav-footer">
|
||||
<a clrVerticalNavLink target="_blank" routerLink="/devcenter">
|
||||
<button type="button" class="btn btn-sm api-button">
|
||||
<span>{{'SIDE_NAV.API_EXPLORER' | translate }}</span>
|
||||
|
@ -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;
|
||||
|
@ -310,13 +310,13 @@
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="http_authproxy_always_onboard"
|
||||
class="required">{{'CONFIG.HTTP_AUTH.ALWAYS_ONBOARD' | translate}}</label>
|
||||
<label for="http_authproxy_skip_search"
|
||||
class="required">{{'CONFIG.HTTP_AUTH.SKIP_SEARCH' | translate}}</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox name="http_authproxy_always_onboard"
|
||||
id="http_authproxy_always_onboard"
|
||||
[disabled]="!currentConfig.http_authproxy_always_onboard.editable"
|
||||
[(ngModel)]="currentConfig.http_authproxy_always_onboard.value" />
|
||||
<input type="checkbox" clrCheckbox name="http_authproxy_skip_search"
|
||||
id="http_authproxy_skip_search"
|
||||
[disabled]="!currentConfig.http_authproxy_skip_search.editable"
|
||||
[(ngModel)]="currentConfig.http_authproxy_skip_search.value" />
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -1,11 +1,12 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false">
|
||||
<h3 class="modal-title" *ngIf="mode === 'create'">{{'GROUP.IMPORT_LDAP_GROUP' | translate}}</h3>
|
||||
<h3 class="modal-title" *ngIf="mode === 'create' && isLdapMode">{{'GROUP.IMPORT_LDAP_GROUP' | translate}}</h3>
|
||||
<h3 class="modal-title" *ngIf="mode === 'create' && isHttpAuthMode">{{'GROUP.IMPORT_HTTP_GROUP' | translate}}</h3>
|
||||
<h3 class="modal-title" *ngIf="mode !== 'create'">{{'GROUP.EDIT' | translate}}</h3>
|
||||
|
||||
<div class="modal-body">
|
||||
<form class="form" #groupForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="isLdapMode">
|
||||
<label for="ldap_group_dn" class="required">{{ 'GROUP.GROUP_DN' | translate}}</label>
|
||||
<label for="ldap_group_dn"
|
||||
aria-haspopup="true"
|
||||
@ -22,7 +23,7 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="isLdapMode">
|
||||
<label for="type">{{'GROUP.TYPE' | translate}}</label>
|
||||
<label id="type">LDAP</label>
|
||||
</div>
|
||||
|
@ -1,13 +1,15 @@
|
||||
|
||||
import {finalize} from 'rxjs/operators';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core";
|
||||
import { NgForm } from "@angular/forms";
|
||||
import { GroupType } from "@harbor/ui";
|
||||
|
||||
import { GroupService } from "../group.service";
|
||||
import { MessageHandlerService } from "./../../shared/message-handler/message-handler.service";
|
||||
import { SessionService } from "./../../shared/session.service";
|
||||
import { UserGroup } from "./../group";
|
||||
import { AppConfigService } from "../../app-config.service";
|
||||
|
||||
@Component({
|
||||
selector: "hbr-add-group-modal",
|
||||
@ -19,7 +21,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
mode = "create";
|
||||
dnTooltip = 'TOOLTIP.ITEM_REQUIRED';
|
||||
|
||||
group: UserGroup = new UserGroup();
|
||||
group: UserGroup;
|
||||
|
||||
formChangeSubscription: Subscription;
|
||||
|
||||
@ -30,25 +32,36 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Output() dataChange = new EventEmitter();
|
||||
|
||||
isLdapMode: boolean;
|
||||
isHttpAuthMode: boolean;
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private msgHandler: MessageHandlerService,
|
||||
private appConfigService: AppConfigService,
|
||||
private groupService: GroupService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
) { }
|
||||
|
||||
ngOnInit() { }
|
||||
ngOnInit() {
|
||||
if (this.appConfigService.isLdapMode()) {
|
||||
this.isLdapMode = true;
|
||||
}
|
||||
if (this.appConfigService.isHttpAuthMode()) {
|
||||
this.isHttpAuthMode = true;
|
||||
}
|
||||
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : GroupType.HTTP_TYPE);
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy() { }
|
||||
|
||||
public get isDNInvalid(): boolean {
|
||||
let dnControl = this.groupForm.controls['ldap_group_dn'];
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
}
|
||||
public get isNameInvalid(): boolean {
|
||||
let dnControl = this.groupForm.controls['group_name'];
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
|
||||
}
|
||||
|
||||
public get isFormValid(): boolean {
|
||||
@ -83,7 +96,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
let groupCopy = Object.assign({}, this.group);
|
||||
this.groupService
|
||||
.createGroup(groupCopy).pipe(
|
||||
finalize(() => this.close()))
|
||||
finalize(() => this.close()))
|
||||
.subscribe(
|
||||
res => {
|
||||
this.msgHandler.showSuccess("GROUP.ADD_GROUP_SUCCESS");
|
||||
@ -97,7 +110,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
let groupCopy = Object.assign({}, this.group);
|
||||
this.groupService
|
||||
.editGroup(groupCopy).pipe(
|
||||
finalize(() => this.close()))
|
||||
finalize(() => this.close()))
|
||||
.subscribe(
|
||||
res => {
|
||||
this.msgHandler.showSuccess("GROUP.EDIT_GROUP_SUCCESS");
|
||||
@ -108,7 +121,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
resetGroup() {
|
||||
this.group = new UserGroup();
|
||||
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : GroupType.HTTP_TYPE);
|
||||
this.groupForm.reset();
|
||||
}
|
||||
}
|
||||
|
@ -15,18 +15,18 @@
|
||||
<clr-icon shape="plus" size="15"></clr-icon> {{'GROUP.ADD' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="editGroup()" [disabled]="!canEditGroup">
|
||||
<clr-icon shape="pencil" size="15"></clr-icon> {{'GROUP.EDIT' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="openDeleteConfirmationDialog()" [disabled]="!canEditGroup">
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="openDeleteConfirmationDialog()" [disabled]="!canDeleteGroup">
|
||||
<clr-icon shape="times" size="15"></clr-icon> {{'GROUP.DELETE' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
|
||||
<clr-dg-column>{{'GROUP.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'GROUP.TYPE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'GROUP.DN' | translate}}</clr-dg-column>
|
||||
<clr-dg-column *ngIf="isLdapMode">{{'GROUP.DN' | translate}}</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let group of groups" [clrDgItem]="group">
|
||||
<clr-dg-cell>{{group.group_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{groupToSring(group.group_type) | translate}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{group.ldap_group_dn}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="isLdapMode">{{group.ldap_group_dn}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="15">
|
||||
|
@ -4,7 +4,7 @@ import { flatMap, catchError } from "rxjs/operators";
|
||||
import { SessionService } from "./../shared/session.service";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { Component, OnInit, ViewChild, OnDestroy } from "@angular/core";
|
||||
import { operateChanges, OperateInfo, OperationService, OperationState, errorHandler as errorHandFn } from "@harbor/ui";
|
||||
import { operateChanges, OperateInfo, OperationService, OperationState, errorHandler as errorHandFn, GroupType } from "@harbor/ui";
|
||||
|
||||
import {
|
||||
ConfirmationTargets,
|
||||
@ -19,6 +19,8 @@ import { UserGroup } from "./group";
|
||||
import { GroupService } from "./group.service";
|
||||
import { MessageHandlerService } from "../shared/message-handler/message-handler.service";
|
||||
import { throwError as observableThrowError } from "rxjs";
|
||||
import { AppConfigService } from '../app-config.service';
|
||||
|
||||
@Component({
|
||||
selector: "app-group",
|
||||
templateUrl: "./group.component.html",
|
||||
@ -35,6 +37,7 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
delSub: Subscription;
|
||||
batchOps = 'idle';
|
||||
batchInfos = new Map();
|
||||
isLdapMode: boolean;
|
||||
|
||||
@ViewChild(AddGroupModalComponent) newGroupModal: AddGroupModalComponent;
|
||||
|
||||
@ -46,10 +49,14 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
private msgHandler: MessageHandlerService,
|
||||
private session: SessionService,
|
||||
private translateService: TranslateService,
|
||||
private appConfigService: AppConfigService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.loadData();
|
||||
if (this.appConfigService.isLdapMode()) {
|
||||
this.isLdapMode = true;
|
||||
}
|
||||
this.delSub = this.operateDialogService.confirmationConfirm$.subscribe(
|
||||
message => {
|
||||
if (
|
||||
@ -150,7 +157,13 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
groupToSring(type: number) {
|
||||
if (type === 1) { return 'GROUP.LDAP_TYPE'; } else { return 'UNKNOWN'; }
|
||||
if (type === GroupType.LDAP_TYPE) {
|
||||
return 'GROUP.LDAP_TYPE';
|
||||
} else if (type === GroupType.HTTP_TYPE) {
|
||||
return 'GROUP.HTTP_TYPE';
|
||||
} else {
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
doFilter(groupName: string): void {
|
||||
@ -162,6 +175,12 @@ export class GroupComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get canEditGroup(): boolean {
|
||||
return (
|
||||
this.selectedGroups.length === 1 &&
|
||||
this.session.currentUser.has_admin_role && this.isLdapMode
|
||||
);
|
||||
}
|
||||
get canDeleteGroup(): boolean {
|
||||
return (
|
||||
this.selectedGroups.length === 1 &&
|
||||
this.session.currentUser.has_admin_role
|
||||
|
@ -4,9 +4,9 @@ export class UserGroup {
|
||||
group_type: number;
|
||||
ldap_group_dn?: string;
|
||||
|
||||
constructor() {
|
||||
constructor(groupType) {
|
||||
{
|
||||
this.group_type = 1;
|
||||
this.group_type = groupType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export class AddGroupComponent implements OnInit {
|
||||
currentTerm = '';
|
||||
|
||||
selectedRole = 1;
|
||||
group = new UserGroup();
|
||||
group = new UserGroup(1);
|
||||
selectedGroups: UserGroup[] = [];
|
||||
groups: UserGroup[] = [];
|
||||
totalCount = 0;
|
||||
@ -89,7 +89,7 @@ export class AddGroupComponent implements OnInit {
|
||||
|
||||
resetModaldata() {
|
||||
this.createGroupMode = false;
|
||||
this.group = new UserGroup();
|
||||
this.group = new UserGroup(1);
|
||||
this.selectedRole = 1;
|
||||
this.selectedGroups = [];
|
||||
this.groups = [];
|
||||
|
@ -0,0 +1,36 @@
|
||||
<clr-modal [(clrModalOpen)]="addHttpAuthOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{'GROUP.NEW_MEMBER' | translate}}</h3>
|
||||
<inline-alert class="modal-title padding-0"></inline-alert>
|
||||
<div class="modal-body">
|
||||
<label>{{ 'GROUP.NEW_USER_INFO' | translate}}</label>
|
||||
|
||||
<form #memberForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="member_name" class="col-md-4 form-group-label-override required">{{'GROUP.GROUP' | translate}} {{'GROUP.NAME' | translate}}</label>
|
||||
<label for="member_name" aria-haspopup="true" role="tooltip"
|
||||
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
|
||||
<input type="text" id="member_name" [(ngModel)]="member_group.group_name"
|
||||
name="member_name"
|
||||
size="20"
|
||||
minlength="3"
|
||||
#memberName="ngModel"
|
||||
required autocomplete="off">
|
||||
</label>
|
||||
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'GROUP.ROLE' | translate}}</label>
|
||||
<div class="radio" *ngFor="let projectRoot of projectRoots">
|
||||
<input type="radio" name="member_role" id="{{'check_root_project_' + projectRoot.NAME}}" [value]="projectRoot.VALUE" [(ngModel)]="role_id">
|
||||
<label for="{{'check_root_project_' + projectRoot.NAME}}">{{ projectRoot.LABEL | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -0,0 +1,8 @@
|
||||
.form-group-label-override {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.padding-0 {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddHttpAuthGroupComponent } from './add-http-auth-group.component';
|
||||
|
||||
describe('AddHttpAuthGroupComponent', () => {
|
||||
let component: AddHttpAuthGroupComponent;
|
||||
let fixture: ComponentFixture<AddHttpAuthGroupComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddHttpAuthGroupComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddHttpAuthGroupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,120 @@
|
||||
import { finalize } from 'rxjs/operators';
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
EventEmitter,
|
||||
Output,
|
||||
ViewChild,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { InlineAlertComponent } from '../../../shared/inline-alert/inline-alert.component';
|
||||
import { UserService } from '../../../user/user.service';
|
||||
|
||||
|
||||
import { errorHandler as errorHandFn, PROJECT_ROOTS, ProjectRootInterface } from "@harbor/ui";
|
||||
|
||||
import { MemberService } from '../member.service';
|
||||
import { UserGroup } from "./../../../group/group";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'add-http-auth-group',
|
||||
templateUrl: './add-http-auth-group.component.html',
|
||||
styleUrls: ['./add-http-auth-group.component.scss'],
|
||||
providers: [UserService]
|
||||
})
|
||||
|
||||
export class AddHttpAuthGroupComponent implements OnInit {
|
||||
projectRoots: ProjectRootInterface[];
|
||||
member_group: UserGroup = { group_name: '', group_type: 2 };
|
||||
role_id: number;
|
||||
addHttpAuthOpened: boolean;
|
||||
|
||||
memberForm: NgForm;
|
||||
|
||||
staticBackdrop: boolean = true;
|
||||
closable: boolean = false;
|
||||
|
||||
@ViewChild('memberForm')
|
||||
currentForm: NgForm;
|
||||
|
||||
@ViewChild(InlineAlertComponent)
|
||||
inlineAlert: InlineAlertComponent;
|
||||
|
||||
@Input() projectId: number;
|
||||
@Output() added = new EventEmitter<boolean>();
|
||||
|
||||
checkOnGoing: boolean = false;
|
||||
|
||||
constructor(private memberService: MemberService,
|
||||
private translateService: TranslateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectRoots = PROJECT_ROOTS;
|
||||
}
|
||||
|
||||
createGroupAsMember() {
|
||||
this.checkOnGoing = true;
|
||||
this.memberService.addGroupMember(this.projectId, this.member_group, this.role_id)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.checkOnGoing = false;
|
||||
}
|
||||
))
|
||||
.subscribe(
|
||||
res => {
|
||||
this.role_id = null;
|
||||
this.addHttpAuthOpened = false;
|
||||
this.added.emit(true);
|
||||
},
|
||||
err => {
|
||||
let errorMessageKey: string = errorHandFn(err);
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(errorMessage => this.inlineAlert.showInlineError(errorMessage));
|
||||
this.added.emit(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
onSubmit(): void {
|
||||
this.createGroupAsMember();
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.role_id = null;
|
||||
this.addHttpAuthOpened = false;
|
||||
}
|
||||
|
||||
|
||||
openAddMemberModal(): void {
|
||||
this.currentForm.reset();
|
||||
this.addHttpAuthOpened = true;
|
||||
this.role_id = 1;
|
||||
}
|
||||
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.currentForm &&
|
||||
this.currentForm.valid &&
|
||||
!this.checkOnGoing;
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
<button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasCreateMemberPermission">
|
||||
<span><clr-icon shape="plus" size="16"></clr-icon> {{'MEMBER.USER' | translate }}</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" (click)="openAddGroupModal()" [disabled]="!hasCreateMemberPermission || !isLdapMode">
|
||||
<button class="btn btn-sm btn-secondary" (click)="openAddGroupModal()" [disabled]="!hasCreateMemberPermission || !(isLdapMode || isHttpAuthMode)">
|
||||
<span><clr-icon shape="plus" size="16"></clr-icon> {{'MEMBER.LDAP_GROUP' | translate}}</span>
|
||||
</button>
|
||||
<clr-dropdown id='member-action' [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-link" clrDropdownTrigger>
|
||||
@ -53,4 +53,5 @@
|
||||
</div>
|
||||
<add-member [projectId]="projectId" [memberList]="members" (added)="addedMember($event)"></add-member>
|
||||
<add-group [projectId]="projectId" [memberList]="members" (added)="addedGroup($event)"></add-group>
|
||||
<add-http-auth-group [projectId]="projectId" (added)="addedGroup($event)"></add-http-auth-group>
|
||||
</div>
|
@ -17,8 +17,10 @@ import { Component, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, Chang
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subscription, forkJoin, Observable } from "rxjs";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { operateChanges, OperateInfo, OperationService, OperationState, UserPermissionService, USERSTATICPERMISSION, ErrorHandler
|
||||
, errorHandler as errorHandFn } from "@harbor/ui";
|
||||
import {
|
||||
operateChanges, OperateInfo, OperationService, OperationState, UserPermissionService, USERSTATICPERMISSION, ErrorHandler
|
||||
, errorHandler as errorHandFn
|
||||
} from "@harbor/ui";
|
||||
|
||||
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
|
||||
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from "../../shared/shared.const";
|
||||
@ -30,6 +32,7 @@ import { Project } from "../../project/project";
|
||||
import { Member } from "./member";
|
||||
import { SessionUser } from "../../shared/session-user";
|
||||
import { AddGroupComponent } from './add-group/add-group.component';
|
||||
import { AddHttpAuthGroupComponent } from './add-http-auth-group/add-http-auth-group.component';
|
||||
import { MemberService } from "./member.service";
|
||||
import { AddMemberComponent } from "./add-member/add-member.component";
|
||||
import { AppConfigService } from "../../app-config.service";
|
||||
@ -56,16 +59,18 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
isDelete = false;
|
||||
isChangeRole = false;
|
||||
loading = false;
|
||||
isLdapMode: boolean = false;
|
||||
|
||||
isChangingRole = false;
|
||||
batchChangeRoleInfos = {};
|
||||
|
||||
isLdapMode: boolean;
|
||||
isHttpAuthMode: boolean;
|
||||
@ViewChild(AddMemberComponent)
|
||||
addMemberComponent: AddMemberComponent;
|
||||
|
||||
@ViewChild(AddGroupComponent)
|
||||
addGroupComponent: AddGroupComponent;
|
||||
@ViewChild(AddHttpAuthGroupComponent)
|
||||
addHttpAuthGroupComponent: AddHttpAuthGroupComponent;
|
||||
hasCreateMemberPermission: boolean;
|
||||
hasUpdateMemberPermission: boolean;
|
||||
hasDeleteMemberPermission: boolean;
|
||||
@ -108,13 +113,15 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
// Get current user from registered resolver.
|
||||
this.currentUser = this.session.getCurrentUser();
|
||||
this.retrieve(this.projectId, "");
|
||||
// get member permission rule
|
||||
this.getMemberPermissionRule(this.projectId);
|
||||
if (this.appConfigService.isLdapMode()) {
|
||||
this.isLdapMode = true;
|
||||
}
|
||||
// get member permission rule
|
||||
this.getMemberPermissionRule(this.projectId);
|
||||
if (this.appConfigService.isHttpAuthMode()) {
|
||||
this.isHttpAuthMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
doSearch(searchMember: string) {
|
||||
this.searchMember = searchMember;
|
||||
this.retrieve(this.projectId, this.searchMember);
|
||||
@ -172,7 +179,11 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Add group
|
||||
openAddGroupModal() {
|
||||
this.addGroupComponent.open();
|
||||
if (this.isLdapMode) {
|
||||
this.addGroupComponent.open();
|
||||
} else {
|
||||
this.addHttpAuthGroupComponent.openAddMemberModal();
|
||||
}
|
||||
}
|
||||
addedGroup(result: boolean) {
|
||||
this.searchMember = "";
|
||||
@ -188,10 +199,10 @@ export class MemberComponent implements OnInit, OnDestroy {
|
||||
return this.memberService
|
||||
.changeMemberRole(projectId, member.id, roleId)
|
||||
.pipe(map(() => this.batchChangeRoleInfos[member.id] = 'done')
|
||||
, catchError(error => {
|
||||
this.messageHandlerService.handleError(error + ": " + member.entity_name);
|
||||
return observableThrowError(error);
|
||||
}));
|
||||
, catchError(error => {
|
||||
this.messageHandlerService.handleError(error + ": " + member.entity_name);
|
||||
return observableThrowError(error);
|
||||
}));
|
||||
};
|
||||
|
||||
// Preparation for members role change
|
||||
|
@ -38,6 +38,7 @@ import { ProjectLabelComponent } from "../project/project-label/project-label.co
|
||||
import { HelmChartModule } from './helm-chart/helm-chart.module';
|
||||
import { RobotAccountComponent } from './robot-account/robot-account.component';
|
||||
import { AddRobotComponent } from './robot-account/add-robot/add-robot.component';
|
||||
import { AddHttpAuthGroupComponent } from './member/add-http-auth-group/add-http-auth-group.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -59,7 +60,8 @@ import { AddRobotComponent } from './robot-account/add-robot/add-robot.component
|
||||
ProjectLabelComponent,
|
||||
AddGroupComponent,
|
||||
RobotAccountComponent,
|
||||
AddRobotComponent
|
||||
AddRobotComponent,
|
||||
AddHttpAuthGroupComponent
|
||||
],
|
||||
exports: [ProjectComponent, ListProjectComponent],
|
||||
providers: [ProjectRoutingResolver, MemberService, RobotService]
|
||||
|
@ -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",
|
||||
@ -769,7 +779,7 @@
|
||||
"HTTP_AUTH": {
|
||||
"ENDPOINT": "Server Endpoint",
|
||||
"TOKEN_REVIEW": "Token Review Endpoint",
|
||||
"ALWAYS_ONBOARD": "Always Onboard",
|
||||
"SKIP_SEARCH": "Skip Search",
|
||||
"VERIFY_CERT": "Verify Certificate"
|
||||
},
|
||||
"OIDC": {
|
||||
@ -1014,6 +1024,26 @@
|
||||
"MSG_SUCCESS": "Retag successfully",
|
||||
"TIP_REPO": "A repository name is broken up into path components. A component of a repository name must be at least one lowercase, alpha-numeric characters, optionally separated by periods, dashes or underscores. More strictly, it must match the regular expression [a-z0-9]+(?:[._-][a-z0-9]+)*.If a repository name has two or more path components, they must be separated by a forward slash ('/').The total length of a repository name, including slashes, must be less the 256 characters.",
|
||||
"TIP_TAG": "A tag is a label applied to a Docker image in a repository. Tags are how various images in a repository are distinguished from each other.It need to match Regex: (`[\\w][\\w.-]{0,127}`)"
|
||||
},
|
||||
"CVE_WHITELIST": {
|
||||
"DEPLOYMENT_SECURITY": "Deployment security",
|
||||
"CVE_WHITELIST": "CVE whitelist",
|
||||
"SYS_WHITELIST_EXPLAIN": "System whitelist allows vulnerabilities in this list to be ignored when calculating the vulnerability of an image.",
|
||||
"ADD_SYS": "Add CVE IDs to the system whitelist",
|
||||
"WARNING_SYS": "The system CVE whitelist has expired. You can enable the whitelist by extending the expiration date.",
|
||||
"WARNING_PRO": "The project CVE whitelist has expired. You can enable the whitelist by extending the expiration date.",
|
||||
"ADD": "ADD",
|
||||
"ENTER": "Enter CVE ID(s)",
|
||||
"HELP": "Separator: commas or newline characters",
|
||||
"NONE": "None",
|
||||
"EXPIRES_AT": "Expires at",
|
||||
"NEVER_EXPIRES": "Never expires",
|
||||
"PRO_WHITELIST_EXPLAIN": "Project whitelist allows vulnerabilities in this list to be ignored in this project when pushing and pulling images.",
|
||||
"PRO_OR_SYS": "Use the system whitelist as is or select “Project whitelist” to create a new whitelist.",
|
||||
"MERGE_INTO": "Merge the system whitelist into this project, add individual CVE IDs.",
|
||||
"SYS_WHITELIST": "System whitelist",
|
||||
"PRO_WHITELIST": "Project whitelist",
|
||||
"ADD_SYSTEM": "ADD SYSTEM"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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",
|
||||
@ -769,7 +779,7 @@
|
||||
"HTTP_AUTH": {
|
||||
"ENDPOINT": "Server Endpoint",
|
||||
"TOKEN_REVIEW": "Review Endpoint De Token",
|
||||
"ALWAYS_ONBOARD": "Always Onboard",
|
||||
"SKIP_SEARCH": "Skip Search",
|
||||
"VERIFY_CERT": "Authentication Verify Cert"
|
||||
},
|
||||
"OIDC": {
|
||||
@ -1012,6 +1022,26 @@
|
||||
"MSG_SUCCESS": "Retag successfully",
|
||||
"TIP_REPO": "A repository name is broken up into path components. A component of a repository name must be at least one lowercase, alpha-numeric characters, optionally separated by periods, dashes or underscores. More strictly, it must match the regular expression [a-z0-9]+(?:[._-][a-z0-9]+)*.If a repository name has two or more path components, they must be separated by a forward slash ('/').The total length of a repository name, including slashes, must be less the 256 characters.",
|
||||
"TIP_TAG": "A tag is a label applied to a Docker image in a repository. Tags are how various images in a repository are distinguished from each other.It need to match Regex: (`[\\w][\\w.-]{0,127}`)"
|
||||
},
|
||||
"CVE_WHITELIST": {
|
||||
"DEPLOYMENT_SECURITY": "Deployment security",
|
||||
"CVE_WHITELIST": "CVE whitelist",
|
||||
"SYS_WHITELIST_EXPLAIN": "System whitelist allows vulnerabilities in this list to be ignored when calculating the vulnerability of an image.",
|
||||
"ADD_SYS": "Add CVE IDs to the system whitelist",
|
||||
"WARNING_SYS": "The system CVE whitelist has expired. You can enable the whitelist by extending the expiration date.",
|
||||
"WARNING_PRO": "The project CVE whitelist has expired. You can enable the whitelist by extending the expiration date.",
|
||||
"ADD": "ADD",
|
||||
"ENTER": "Enter CVE ID(s)",
|
||||
"HELP": "Separator: commas or newline characters",
|
||||
"NONE": "None",
|
||||
"EXPIRES_AT": "Expires at",
|
||||
"NEVER_EXPIRES": "Never expires",
|
||||
"PRO_WHITELIST_EXPLAIN": "Project whitelist allows vulnerabilities in this list to be ignored in this project when pushing and pulling images.",
|
||||
"PRO_OR_SYS": "Use the system whitelist as is or select “Project whitelist” to create a new whitelist.",
|
||||
"MERGE_INTO": "Merge the system whitelist into this project, add individual CVE IDs.",
|
||||
"SYS_WHITELIST": "System whitelist",
|
||||
"PRO_WHITELIST": "Project whitelist",
|
||||
"ADD_SYSTEM": "ADD SYSTEM"
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user