fix code conflict and rebase with master

Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
Steven Zou 2019-10-22 18:39:37 +08:00
commit 370a364c29
106 changed files with 1037 additions and 539 deletions

View File

@ -1,23 +1,23 @@
sudo: true
language: go
go:
- 1.12.5
- 1.12.12
go_import_path: github.com/goharbor/harbor
services:
- docker
dist: trusty
matrix:
include:
- go: 1.12.5
- go: 1.12.12
env:
- UTTEST=true
- go: 1.12.5
- go: 1.12.12
env:
- APITEST_DB=true
- go: 1.12.5
- go: 1.12.12
env:
- APITEST_LDAP=true
- go: 1.12.5
- go: 1.12.12
env:
- OFFLINE=true
- language: node_js

View File

@ -80,3 +80,6 @@ feature within Harbor before deploying images into production.
**Allegis:** Harbor is used at Allegis as a secure private registry to store
and scan customized container images for different business applications, like
ELK stack, as part of their CI/CD pipeline.
# Adding a logo
If you would like to add your logo to the `Users and Partners of Harbor` section of the website, add a PNG version of your logo to the docs/img directory in this repo and submit a pull request with your change. Name the image file something that reflects your company (e.g., if your company is called Acme, name the image acme.png). We will follow up and make the change in the goharbor.io website as well.

View File

@ -127,6 +127,7 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
| 1.6 | 1.9.2 |
| 1.7 | 1.9.2 |
| 1.8 | 1.11.2 |
| 1.9 | 1.12.12 |
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.

View File

@ -9,7 +9,7 @@
# compile_golangimage:
# compile from golang image
# for example: make compile_golangimage -e GOBUILDIMAGE= \
# golang:1.11.2
# golang:1.12.12
# compile_core, compile_jobservice: compile specific binary
#
# build: build Harbor docker images from photon baseimage
@ -111,6 +111,9 @@ CLAIRADAPTERVERSION=c7db8b15
# version of chartmuseum
CHARTMUSEUMVERSION=v0.9.0
# version of registry for pulling the source code
REGISTRY_SRC_TAG=v2.7.1
define VERSIONS_FOR_PREPARE
VERSION_TAG: $(VERSIONTAG)
REGISTRY_VERSION: $(REGISTRYVERSION)
@ -138,7 +141,7 @@ GOINSTALL=$(GOCMD) install
GOTEST=$(GOCMD) test
GODEP=$(GOTEST) -i
GOFMT=gofmt -w
GOBUILDIMAGE=golang:1.12.5
GOBUILDIMAGE=golang:1.12.12
GOBUILDPATH=/harbor
GOIMAGEBUILDCMD=/usr/local/go/bin/go
GOIMAGEBUILD=$(GOIMAGEBUILDCMD) build -mod vendor

View File

@ -3,28 +3,6 @@ guidelines.
[GOVERNANCE.md](https://github.com/goharbor/community/blob/master/GOVERNANCE.md)
describes governance guidelines and maintainer responsibilities.
Maintainer list here is a copy of [MAINTAINERS.md](https://github.com/goharbor/community/blob/master/MAINTAINERS.md).
[MAINTAINERS.md](https://github.com/goharbor/community/blob/master/MAINTAINERS.md) contains the project maintainers.
## Core Maintainers
| Core Maintainer | GitHub ID | Affiliation |
| --------------- | --------- | ----------- |
| Daniel Jiang | [reasonerjt](https://github.com/reasonerjt) | [VMware](https://www.github.com/vmware/) |
| Steven Ren | [renmaosheng](https://github.com/renmaosheng) | [VMware](https://www.github.com/vmware/) |
| Steven Zou | [steven-zou](https://github.com/steven-zou) | [VMware](https://www.github.com/vmware/) |
| Henry Zhang| [hainingzhang](https://github.com/hainingzhang)| [VMware](https://www.github.com/vmware/) |
| Michael Michael |[michmike](https://github.com/michmike)| [VMware](https://www.github.com/vmware/) |
## Maintainers
| Maintainer | GitHub ID | Affiliation |
| ---------- | --------- | ----------- |
| Daojun Zhang | [stonezdj](https://github.com/stonezdj) | [VMware](https://www.github.com/vmware/) |
| Wenkai Yin | [ywk253100](https://github.com/ywk253100) | [VMware](https://www.github.com/vmware/) |
| Yan Wang | [wy65701436](https://github.com/wy65701436) | [VMware](https://www.github.com/vmware/) |
| Qian Deng | [ninjadq](https://github.com/ninjadq) | [VMware](https://www.github.com/vmware/) |
| Mia Zhou | [zhoumeina](https://github.com/zhoumeina) | [VMware](https://www.github.com/vmware/) |
| Nathan Lowe | [nlowe](https://github.com/nlowe) | [Hyland Software](https://github.com/HylandSoftware) |
| De Chen | [cd1989](https://github.com/cd1989) | [Caicloud](https://github.com/caicloud) |
| Mingming Pei | [mmpei](https://github.com/mmpei) | [Netease](https://github.com/netease) |
| Fanjian Kong | [kofj](https://github.com/kofj) | [Qihoo360](https://github.com/Qihoo360) |
[GUIDING_PRINCIPLES.md](https://github.com/goharbor/community/blob/master/GUIDING_PRINCIPLES.md) contains the project vision, values and principles and how we apply them in making decisions.

28
RELEASES.md Normal file
View File

@ -0,0 +1,28 @@
# Versioning and Release
This document describes the versioning and release process of Harbor. This document is a living document, contents will be updated according to each releases.
## Releases
Harbor releases will be versioned using dotted triples, similar to [Semantic Version](http://semver.org/). For this specific document, we will refer to the respective components of this triple as `<major>.<minor>.<patch>`. The version number may have additional information, such as "-rc1,-rc2,-rc3" to mark release candidate builds for earlier access. Such releases will be considered as "pre-releases".
### Major and Minor Releases
Major and minor releases of Harbor will be branched from master when the release reaches to `RC(release candidate)` state. The branch format should follow `release-<major>.<minor>.0`. For example, once the release `v1.0.0` reaches to RC, a branch will be created with the format `release-1.0.0`. When the release reaches to `GA(General Available)` state, The tag with format `v<major>.<minor>.<patch>` and should be made with command `git tag -s v<major>.<minor>.<patch>`. The release cadency is around 3 months, might be adjusted based on open source event, but will communicate it clearly.
### Patch releases
Patch releases are based on the major/minor release branch, the release cadency for patch release of recent minor release is one month to solve critical community and security issues. The cadency for patch release of recent minus two minor releases are on-demand driven based on the severity of the issue to be fixed.
### Pre-releases
`Pre-releases:mainly the different RC builds` will be compiled from their corresponding branches. Please note they are done to assist in the stabilization process, no guarantees are provided.
### Minor Release Support Matrix
| Version | Supported |
| ------- | ------------------ |
| Harbor v1.7.x | :white_check_mark: |
| Harbor v1.8.x | :white_check_mark: |
| Harbor v1.9.x | :white_check_mark: |
### Upgrade path and support policy
The upgrade path for Harbor is (1) 1.0.x patch releases are always compatible with its major and minor version. For example, previous released 1.8.x can be upgraded to most recent 1.8.4 release. (2) Harbor only supports two previous minor releases to upgrade to current minor release. For example, 1.9.0 will only support 1.7.0 and 1.8.0 to upgrade from, 1.6.0 to 1.9.0 is not supported. One should upgrade to 1.8.0 first, then to 1.9.0.
The Harbor project maintains release branches for the three most recent minor releases, each minor release will be maintained for approximately 9 months. There is no mandated timeline for major versions and there are currently no criteria for shipping a new major version (i.e. Harbor 2.0.0).
### Next Release
The activity for next release will be tracked in the [up-to-date project board](https://github.com/orgs/goharbor/projects/1). If your issue is not present in the corresponding release, please reach out to the maintainers to add the issue to the project board.

View File

@ -2,20 +2,8 @@
Harbor is a large growing community devoted in creating a private enterprise-grade registry for all your cloud native assets. The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues.
## Supported Versions
This section describes the maximum version skew supported between various Harbor releases. Harbor versions are expressed as **x.y.z**, where **x** is the major version, **y** is the minor version, and **z** is the patch version, following [Semantic Versioning terminology](https://semver.org/).
The Harbor project maintains release branches for the three most recent minor releases. Applicable fixes, including security fixes, may be backported to those three release branches, depending on severity and feasibility. Please refer to [RELEASES.md](https://github.com/goharbor/harbor/blob/master/RELEASES.md) for details.
### Support Policy
The Harbor project maintains release branches for the three most recent minor releases. Applicable fixes, including security fixes, may be backported to those three release branches, depending on severity and feasibility. Patch releases are cut from those branches at a regular cadence, or as needed. The Harbor project typically has a minor release approximately every 3 months, maintaining each minor release branch for approximately 9 months.
There is no mandated timeline for major versions and there are currently no criteria for shipping a new major version (i.e. Harbor 2.0.0).
### Minor Release Support Matrix
| Version | Supported |
| ------- | ------------------ |
| Harbor v1.7.x | :white_check_mark: |
| Harbor v1.8.x | :white_check_mark: |
| Harbor v1.9.x | :white_check_mark: |
## Reporting a Vulnerability - Private Disclosure Process
Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to Harbor privately, to minimize attacks against current users of Harbor before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project.

View File

@ -1 +1 @@
v1.9.0
v1.10.0

View File

@ -43,25 +43,25 @@ You can compile the code by one of the three approaches:
- Get official Golang image from docker hub:
```sh
$ docker pull golang:1.12.5
$ docker pull golang:1.12.12
```
- Build, install and bring up Harbor without Notary:
```sh
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage
```
- Build, install and bring up Harbor with Notary:
```sh
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
```
- Build, install and bring up Harbor with Clair:
```sh
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage CLAIRFLAG=true
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage CLAIRFLAG=true
```
#### II. Compile code with your own Golang environment, then build Harbor

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

View File

@ -10,44 +10,45 @@ System admin have all permissions for the project.
The following table depicts the various user permission levels in a project.
| Action | Guest | Developer | Master | Project Admin |
| --------------------------------------- | ----- | --------- | ------ | ------------- |
| See the porject configurations | ✓ | ✓ | ✓ | ✓ |
| Edit the project configurations | | | | ✓ |
| See a list of project members | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete project members | | | | ✓ |
| See a list of project logs | ✓ | ✓ | ✓ | ✓ |
| See a list of project replications | | | ✓ | ✓ |
| See a list of project replication jobs | | | | ✓ |
| See a list of project labels | | | ✓ | ✓ |
| Create/edit/delete project lables | | | ✓ | ✓ |
| See a list of repositories | ✓ | ✓ | ✓ | ✓ |
| Create repositories | | ✓ | ✓ | ✓ |
| Edit/delete repositories | | | ✓ | ✓ |
| See a list of images | ✓ | ✓ | ✓ | ✓ |
| Retag image | ✓ | ✓ | ✓ | ✓ |
| Pull image | ✓ | ✓ | ✓ | ✓ |
| Push image | | ✓ | ✓ | ✓ |
| Scan/delete image | | | ✓ | ✓ |
| See a list of image vulnerabilities | ✓ | ✓ | ✓ | ✓ |
| See image build history | ✓ | ✓ | ✓ | ✓ |
| Add/Remove labels of image | | ✓ | ✓ | ✓ |
| See a list of helm charts | ✓ | ✓ | ✓ | ✓ |
| Download helm charts | ✓ | ✓ | ✓ | ✓ |
| Upload helm charts | | ✓ | ✓ | ✓ |
| Delete helm charts | | | ✓ | ✓ |
| See a list of helm chart versions | ✓ | ✓ | ✓ | ✓ |
| Download helm chart versions | ✓ | ✓ | ✓ | ✓ |
| Upload helm chart versions | | ✓ | ✓ | ✓ |
| Delete helm chart versions | | | ✓ | ✓ |
| Add/Remove labels of helm chart version | | ✓ | ✓ | ✓ |
| See a list of project robots | | | ✓ | ✓ |
| Create/edit/delete project robots | | | | ✓ |
| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ |
| Create/edit/remove CVE whitelist | | | | ✓ |
| Enable/disable webhooks | | ✓ | ✓ | ✓ |
| Create/delete tag retention rules | | ✓ | ✓ | ✓ |
| Enable/disable tag retention rules | | ✓ | ✓ | ✓ |
| See project quotas | ✓ | ✓ | ✓ | ✓ |
| Edit project quotas | | | | |
| Action | Limited Guest | Guest | Developer | Master | Project Admin |
| --------------------------------------- | ------------- | ----- | --------- | ------ | ------------- |
| See the porject configurations | ✓ | ✓ | ✓ | ✓ | ✓ |
| Edit the project configurations | | | | | ✓ |
| See a list of project members | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete project members | | | | | ✓ |
| See a list of project logs | | ✓ | ✓ | ✓ | ✓ |
| See a list of project replications | | | | ✓ | ✓ |
| See a list of project replication jobs | | | | | ✓ |
| See a list of project labels | | | | ✓ | ✓ |
| Create/edit/delete project lables | | | | ✓ | ✓ |
| See a list of repositories | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create repositories | | | ✓ | ✓ | ✓ |
| Edit/delete repositories | | | | ✓ | ✓ |
| See a list of images | ✓ | ✓ | ✓ | ✓ | ✓ |
| Retag image | | ✓ | ✓ | ✓ | ✓ |
| Pull image | ✓ | ✓ | ✓ | ✓ | ✓ |
| Push image | | | ✓ | ✓ | ✓ |
| Scan/delete image | | | | ✓ | ✓ |
| See a list of image vulnerabilities | ✓ | ✓ | ✓ | ✓ | ✓ |
| See image build history | ✓ | ✓ | ✓ | ✓ | ✓ |
| Add/Remove labels of image | | | ✓ | ✓ | ✓ |
| See a list of helm charts | ✓ | ✓ | ✓ | ✓ | ✓ |
| Download helm charts | ✓ | ✓ | ✓ | ✓ | ✓ |
| Upload helm charts | | | ✓ | ✓ | ✓ |
| Delete helm charts | | | | ✓ | ✓ |
| See a list of helm chart versions | ✓ | ✓ | ✓ | ✓ | ✓ |
| Download helm chart versions | ✓ | ✓ | ✓ | ✓ | ✓ |
| Upload helm chart versions | | | ✓ | ✓ | ✓ |
| Delete helm chart versions | | | | ✓ | ✓ |
| Add/Remove labels of helm chart version | | | ✓ | ✓ | ✓ |
| See a list of project robots | | | | ✓ | ✓ |
| Create/edit/delete project robots | | | | | ✓ |
| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create/edit/remove CVE whitelist | | | | | ✓ |
| Enable/disable webhooks | | | ✓ | ✓ | ✓ |
| Create/delete tag retention rules | | | ✓ | ✓ | ✓ |
| Enable/disable tag retention rules | | | ✓ | ✓ | ✓ |
| See project quotas | ✓ | ✓ | ✓ | ✓ | ✓ |
| Edit project quotas | | | | | |

View File

@ -1,18 +1,32 @@
# Registry Landscape
The cloud native ecosystem is moving rapidlyregistries and their featuresets are no exception. We've made our best effort to survey the container registry landscape and compare to our core featureset.
The cloud native ecosystem is moving rapidlyregistries and their feature sets are no exception. We've made our best effort to survey the container registry landscape and compare to our core feature set.
If you find something outdated or outright erroneous, please submit a PR and we'll fix it right away.
| Feature | Harbor | Docker Trusted Registry | Quay | Cloud Providers (GCP, AWS, Azure) | Docker Distribution | Artifactory |
| -------------: | :----: | :---------------------: | :--: | :-------------------------------: | :-----------------: | :---------: |
| Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
| LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ |
| Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial |
| Vulnerability Scanning & Monitoring | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
| Replication | ✓ | ✓ | ✓ | n/a | ✗ | ✓ |
| Multi-Tenancy (projects, teams, etc.) | ✓ | ✓ | ✓ | partial | ✗ | ✓ |
| Role-Based Access Control | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
| Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? |
| Upstream Registry Proxy Cache | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ |
| Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
Table updated on 10/21/2019 against Harbor 1.9.
| Feature | Harbor | Docker Trusted Registry | Quay | Cloud Providers (GCP, AWS, Azure) | Docker Distribution | Artifactory | GitLab |
| -------------: | :----: | :---------------------: | :-----: | :-------------------------------: | :-----------------: | :---------: | :------: |
| Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? | ? |
| Artifact Repository (rpms, git, jar, etc) | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | partial |
| Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial | ✗ |
| Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| Helm Chart Repository Manager | ✓ | ✗ | partial | ✗ | ✗ | ✓ | ✗ |
| LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ | ✓ |
| Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Multi-Tenancy (projects, teams, namespaces, etc) | ✓ | ✓ | ✓ | partial | ✗ | ✓ | ✓ |
| Open Source | ✓ | partial | ✗ | ✗ | ✓ | partial | partial |
| Project Quotas (by image count & storage consumption) | ✓ | ✗ | ✗ | partial | ✗ | ✗ | ✗ |
| Replication between instances | ✓ | ✓ | ✓ | n/a | ✗ | ✓ | ✗ |
| Replication between non-instances | ✓ | ✗ | ✓ | n/a | ✗ | ✗ | ✗ |
| Robot Accounts for Helm Charts | ✓ | ✗ | ✗ | ? | ✗ | ✗ | ✗ |
| Robot Accounts for Images | ✓ | ? | ✓ | ? | ✗ | ? | ? |
| Role-Based Access Control | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✗ |
| Single Sign On (OIDC) | ✓ | ✓ | ✓ | ✓ | ✗ | partial | ✗ |
| Tag Retention Policy | ✓ | ✗ | partial | ✗ | ✗ | ✗ | ✗ |
| Upstream Registry Proxy Cache | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Vulnerability Scanning & Monitoring | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | partial |
| Vulnerability Scanning Plugin Framework | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Vulnerability Whitelisting | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Webhooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |

View File

@ -1089,6 +1089,8 @@ paths:
description: Forbidden.
'404':
description: Repository not found.
'412':
description: Precondition Failed.
put:
summary: Update description of the repository.
description: |

View File

@ -36,10 +36,10 @@ version | set harbor version
#### EXAMPLE:
#### Build and run harbor from source code.
make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
### Package offline installer
make package_offline GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
make package_offline GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
### Start harbor with notary
make -e NOTARYFLAG=true start

View File

@ -1,11 +1,12 @@
#!/bin/bash
set -e
set +o noglob
DIR="$(cd "$(dirname "$0")" && pwd)"
source $DIR/common.sh
set +o noglob
usage=$'Please set hostname and other necessary attributes in harbor.yml first. DO NOT use localhost or 127.0.0.1 for hostname, because Harbor needs to be accessed by external clients.
Please set --with-notary if needs enable Notary in Harbor, and set ui_url_protocol/ssl_cert/ssl_cert_key in harbor.yml bacause notary must run under https.
Please set --with-clair if needs enable Clair in Harbor

View File

@ -57,4 +57,7 @@ DROP TABLE IF EXISTS img_scan_job;
DROP TRIGGER IF EXISTS TRIGGER ON img_scan_overview;
DROP TABLE IF EXISTS img_scan_overview;
DROP TABLE IF EXISTS clair_vuln_timestamp
DROP TABLE IF EXISTS clair_vuln_timestamp;
/* Add limited guest role */
INSERT INTO role (role_code, name) VALUES ('LRS', 'limitedGuest');

View File

@ -97,7 +97,7 @@ DOCKERIMAGENAME_MIGRATOR=goharbor/harbor-migrator
# for chart server (chartmuseum)
DOCKERFILEPATH_CHART_SERVER=$(DOCKERFILEPATH)/chartserver
DOCKERFILENAME_CHART_SERVER=Dockerfile
CHART_SERVER_CODE_BASE=github.com/helm/chartmuseum
CHART_SERVER_CODE_BASE=https://github.com/helm/chartmuseum.git
CHART_SERVER_MAIN_PATH=cmd/chartmuseum
CHART_SERVER_BIN_NAME=chartm
@ -195,7 +195,7 @@ _build_registry:
rm -rf $(DOCKERFILEPATH_REG)/binary && mkdir -p $(DOCKERFILEPATH_REG)/binary && \
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/registry/release-$(REGISTRYVERSION)/registry, $(DOCKERFILEPATH_REG)/binary/registry); \
else \
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRYVERSION) && cd - ; \
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRY_SRC_TAG) && cd - ; \
fi
@echo "building registry container for photon..."
@chmod 655 $(DOCKERFILEPATH_REG)/binary/registry && $(DOCKERBUILD) -f $(DOCKERFILEPATH_REG)/$(DOCKERFILENAME_REG) -t $(DOCKERIMAGENAME_REG):$(REGISTRYVERSION)-$(VERSIONTAG) .

View File

@ -30,4 +30,4 @@ cp compile.sh binary/
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
#docker rm -f golang_code_builder

View File

@ -1,4 +1,4 @@
FROM golang:1.12.5
FROM golang:1.12.12
ADD . /go/src/github.com/goharbor/harbor-scanner-clair/
WORKDIR /go/src/github.com/goharbor/harbor-scanner-clair/

View File

@ -23,7 +23,7 @@ TEMP=`mktemp -d ${TMPDIR-/tmp}/clair-adapter.XXXXXX`
git clone https://github.com/danielpacak/harbor-scanner-clair.git $TEMP
cd $TEMP; git checkout $VERSION; cd -
echo 'build the clair adapter binary bases on the golang:1.12.5...'
echo 'build the clair adapter binary bases on the golang:1.12.12'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t clair-adapter-golang $TEMP

View File

@ -1,4 +1,4 @@
FROM golang:1.11.2
FROM golang:1.12.12
ADD . /go/src/github.com/coreos/clair/
WORKDIR /go/src/github.com/coreos/clair/

View File

@ -23,7 +23,7 @@ TEMP=`mktemp -d /$TMPDIR/clair.XXXXXX`
git clone https://github.com/coreos/clair.git $TEMP
cd $TEMP; git checkout $VERSION; cd -
echo 'build the clair binary bases on the golang:1.11.2...'
echo 'build the clair binary bases on the golang:1.12.12'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t clair-golang $TEMP

View File

@ -1,4 +1,4 @@
FROM golang:1.11.2
FROM golang:1.12.12
ARG NOTARY_VERSION
ARG MIGRATE_VERSION

View File

@ -24,6 +24,8 @@ docker cp $ID:/go/bin/notary-signer binary/
docker cp $ID:/go/bin/migrate binary/
docker cp $ID:/migrations binary/
sed -i 's/waiting for $DB_URL/waiting for database/g' binary/migrations/migrate.sh
docker rm -f $ID
docker rmi -f notary-binary

View File

@ -1,4 +1,4 @@
FROM golang:1.11
FROM golang:1.12.12
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV BUILDTAGS include_oss include_gcs

View File

@ -29,7 +29,7 @@ wget https://github.com/docker/distribution/pull/2879.patch
git apply 2879.patch
cd $cur
echo 'build the registry binary bases on the golang:1.11...'
echo 'build the registry binary ...'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t registry-golang $TEMP

View File

@ -18,6 +18,7 @@ package metadata
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
@ -139,12 +140,12 @@ type Int64Type struct {
}
func (t *Int64Type) validate(str string) error {
_, err := strconv.ParseInt(str, 10, 64)
_, err := parseInt64(str)
return err
}
func (t *Int64Type) get(str string) (interface{}, error) {
return strconv.ParseInt(str, 10, 64)
return parseInt64(str)
}
// BoolType ...
@ -194,7 +195,7 @@ type QuotaType struct {
}
func (t *QuotaType) validate(str string) error {
val, err := strconv.ParseInt(str, 10, 64)
val, err := parseInt64(str)
if err != nil {
return err
}
@ -205,3 +206,18 @@ func (t *QuotaType) validate(str string) error {
return nil
}
// parseInt64 returns int64 from string which support scientific notation
func parseInt64(str string) (int64, error) {
val, err := strconv.ParseInt(str, 10, 64)
if err == nil {
return val, nil
}
fval, err := strconv.ParseFloat(str, 64)
if err == nil && fval == math.Trunc(fval) {
return int64(fval), nil
}
return 0, fmt.Errorf("invalid int64 string: %s", str)
}

View File

@ -15,8 +15,9 @@
package metadata
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIntType_validate(t *testing.T) {
@ -96,3 +97,33 @@ func TestMapType_get(t *testing.T) {
result, _ := test.get(`{"sample":"abc", "another":"welcome"}`)
assert.Equal(t, map[string]interface{}{"sample": "abc", "another": "welcome"}, result)
}
func Test_parseInt64(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want int64
wantErr bool
}{
{"1", args{"1"}, int64(1), false},
{"1.0", args{"1.0"}, int64(1), false},
{"1.1", args{"1.1"}, int64(0), true},
{"1E2", args{"1E2"}, int64(100), false},
{"1.073741824e+11", args{"1.073741824e+11"}, int64(107374182400), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseInt64(tt.args.str)
if (err != nil) != tt.wantErr {
t.Errorf("parseInt64() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseInt64() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -33,6 +33,7 @@ const (
RoleDeveloper = 2
RoleGuest = 3
RoleMaster = 4
RoleLimitedGuest = 5
LabelLevelSystem = "s"
LabelLevelUser = "u"

View File

@ -78,7 +78,7 @@ func addProjectMember(member models.Member) (int, error) {
func GetProjectByID(id int64) (*models.Project, error) {
o := GetOrmer()
sql := `select p.project_id, p.name, u.username as owner_name, p.owner_id, p.creation_time, p.update_time
sql := `select p.project_id, p.name, u.username as owner_name, p.owner_id, p.creation_time, p.update_time
from project p left join harbor_user u on p.owner_id = u.user_id where p.deleted = false and p.project_id = ?`
queryParam := make([]interface{}, 1)
queryParam = append(queryParam, id)
@ -142,7 +142,7 @@ func GetTotalOfProjects(query *models.ProjectQueryParam) (int64, error) {
// GetProjects returns a project list according to the query conditions
func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
sqlStr, queryParam := projectQueryConditions(query)
sqlStr = `select distinct p.project_id, p.name, p.owner_id,
sqlStr = `select distinct p.project_id, p.name, p.owner_id,
p.creation_time, p.update_time ` + sqlStr + ` order by p.name`
sqlStr, queryParam = CreatePagination(query, sqlStr, queryParam)
@ -158,15 +158,15 @@ func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
// and the user is in the group which is a group member of this project.
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,
sql = `select distinct p.project_id, p.name, p.owner_id,
p.creation_time, p.update_time ` + sql
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
`%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'
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
where ug.id in ( %s )`,
sql, groupIDCondition)
}
@ -188,11 +188,11 @@ func GetTotalGroupProjects(groupIDs []int, query *models.ProjectQueryParam) (int
sql = `select count(1) ` + sqlCondition
} else {
sql = fmt.Sprintf(
`select count(1)
from ( select p.project_id %s union select p.project_id
from project p
`select count(1)
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'
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
where ug.id in ( %s )) t`,
sqlCondition, groupIDCondition)
}
@ -254,6 +254,8 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac
roleID = 3
case common.RoleMaster:
roleID = 4
case common.RoleLimitedGuest:
roleID = 5
}
params = append(params, roleID)
}
@ -287,8 +289,8 @@ func DeleteProject(id int64) error {
return err
}
name := fmt.Sprintf("%s#%d", project.Name, project.ProjectID)
sql := `update project
set deleted = true, name = ?
sql := `update project
set deleted = true, name = ?
where project_id = ?`
_, err = GetOrmer().Raw(sql, name, id).Exec()
return err
@ -304,8 +306,8 @@ func GetRolesByGroupID(projectID int64, groupIDs []int) ([]int, error) {
groupIDCondition := JoinNumberConditions(groupIDs)
o := GetOrmer()
sql := fmt.Sprintf(
`select distinct pm.role from project_member pm
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id
`select distinct pm.role from project_member pm
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id
where ug.id in ( %s ) and pm.project_id = ?`,
groupIDCondition)
log.Debugf("sql for GetRolesByGroupID(project ID: %d, group ids: %v):%v", projectID, groupIDs, sql)

View File

@ -201,6 +201,7 @@ type ProjectSummary struct {
MasterCount int64 `json:"master_count"`
DeveloperCount int64 `json:"developer_count"`
GuestCount int64 `json:"guest_count"`
LimitedGuestCount int64 `json:"limited_guest_count"`
Quota struct {
Hard types.ResourceList `json:"hard"`

View File

@ -73,6 +73,7 @@ type TagDetail struct {
Author string `json:"author"`
Created time.Time `json:"created"`
Config *TagCfg `json:"config"`
Immutable bool `json:"immutable"`
}
// TagCfg ...

View File

@ -43,6 +43,7 @@ const (
ResourceLog = Resource("log")
ResourceMember = Resource("member")
ResourceMetadata = Resource("metadata")
ResourceQuota = Resource("quota")
ResourceReplication = Resource("replication") // TODO remove
ResourceReplicationJob = Resource("replication-job") // TODO remove
ResourceReplicationExecution = Resource("replication-execution")

View File

@ -48,124 +48,7 @@ var (
}
// all policies for the projects
allPolicies = []*rbac.Policy{
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceSelf, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionRead},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete},
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
{Resource: rbac.ResourceReplication, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceReplication, Action: rbac.ActionDelete},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionDelete},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionRead},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceLabelResource, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRobot, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRobot, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionCreate},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionDelete},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionList},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionRead},
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
}
allPolicies = computeAllPolicies()
)
// PoliciesForPublicProject ...
@ -197,3 +80,19 @@ func GetAllPolicies(namespace rbac.Namespace) []*rbac.Policy {
return policies
}
func computeAllPolicies() []*rbac.Policy {
var results []*rbac.Policy
mp := map[string]bool{}
for _, policies := range rolePoliciesMap {
for _, policy := range policies {
if !mp[policy.String()] {
results = append(results, policy)
mp[policy.String()] = true
}
}
}
return results
}

View File

@ -53,6 +53,8 @@ var (
{Resource: rbac.ResourceLabelResource, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
@ -137,6 +139,8 @@ var (
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
@ -220,6 +224,8 @@ var (
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
@ -273,6 +279,8 @@ var (
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
@ -299,6 +307,30 @@ var (
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
},
"limitedGuest": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
},
}
)
@ -319,6 +351,8 @@ func (role *visitorRole) GetRoleName() string {
return "developer"
case common.RoleGuest:
return "guest"
case common.RoleLimitedGuest:
return "limitedGuest"
default:
return ""
}

View File

@ -35,6 +35,9 @@ func (suite *VisitorRoleTestSuite) TestGetRoleName() {
guest := visitorRole{roleID: common.RoleGuest}
suite.Equal(guest.GetRoleName(), "guest")
limitedGuest := visitorRole{roleID: common.RoleLimitedGuest}
suite.Equal(limitedGuest.GetRoleName(), "limitedGuest")
unknow := visitorRole{roleID: 404}
suite.Equal(unknow.GetRoleName(), "")
}

View File

@ -125,6 +125,8 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
roles = append(roles, common.RoleDeveloper)
case "RS":
roles = append(roles, common.RoleGuest)
case "LRS":
roles = append(roles, common.RoleLimitedGuest)
}
}
return mergeRoles(roles, s.GetRolesByGroup(projectIDOrName))

View File

@ -221,26 +221,6 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
u.GroupDNList = groupDNList
}
log.Debugf("Searching for nested groups")
nestedGroupDNList := []string{}
nestedGroupFilter := createNestedGroupFilter(ldapEntry.DN)
result, err := session.SearchLdap(nestedGroupFilter)
if err != nil {
return nil, err
}
for _, groupEntry := range result.Entries {
if !contains(u.GroupDNList, groupEntry.DN) {
nestedGroupDNList = append(nestedGroupDNList, strings.TrimSpace(groupEntry.DN))
log.Debugf("Found group %v", groupEntry.DN)
} else {
log.Debugf("%v is already in GroupDNList", groupEntry.DN)
}
}
u.GroupDNList = append(u.GroupDNList, nestedGroupDNList...)
log.Debugf("Done searching for nested groups")
u.DN = ldapEntry.DN
ldapUsers = append(ldapUsers, u)
@ -442,12 +422,6 @@ func createGroupSearchFilter(oldFilter, groupName, groupNameAttribute string) st
return filter
}
func createNestedGroupFilter(userDN string) string {
filter := ""
filter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:=" + goldap.EscapeFilter(userDN) + "))"
return filter
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {

View File

@ -106,7 +106,7 @@ var insecureTransport = &http.Transport{
// Token wraps the attributes of a oauth2 token plus the attribute of ID token
type Token struct {
oauth2.Token
IDToken string `json:"id_token"`
IDToken string `json:"id_token,omitempty"`
}
func getOauthConf() (*oauth2.Config, error) {
@ -200,9 +200,10 @@ func RefreshToken(ctx context.Context, token *Token) (*Token, error) {
if err != nil {
return nil, err
}
it, ok := t.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("failed to get id_token from refresh response")
log.Debugf("id_token not exist in refresh response")
}
return &Token{Token: *t, IDToken: it}, nil
}

View File

@ -11,6 +11,7 @@ import (
"github.com/goharbor/harbor/src/core/config"
"github.com/pkg/errors"
"sync"
"time"
)
// SecretVerifyError wraps the different errors happened when verifying a secret for OIDC user. When seeing this error,
@ -83,7 +84,10 @@ func (dm *defaultManager) VerifySecret(ctx context.Context, userID int, secret s
return dm.VerifyToken(ctx, oidcUser)
}
// VerifyToken verifies the token in the model from parm in this implementation it will try to refresh the token
// VerifyToken checks the expiration of the token in the model, (we'll only do expiration checks b/c according to spec,
// the response may not have ID token:
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
// and it will try to refresh the token
// if it's expired, if the refresh is successful it will persist the token and consider the verification successful.
func (dm *defaultManager) VerifyToken(ctx context.Context, user *models.OIDCUser) error {
if user == nil {
@ -103,11 +107,10 @@ func (dm *defaultManager) VerifyToken(ctx context.Context, user *models.OIDCUser
return verifyError(err)
}
log.Debugf("Token string for verify: %s", tokenStr)
_, err = VerifyToken(ctx, token.IDToken)
if err == nil {
if token.Expiry.After(time.Now()) {
return nil
}
log.Infof("Failed to verify ID Token, error: %v, refreshing...", err)
log.Info("Token string has expired, refreshing...")
t, err := RefreshToken(ctx, token)
if err != nil {
return verifyError(err)

View File

@ -593,8 +593,18 @@ func (p *ProjectAPI) Summary() {
ChartCount: p.project.ChartCount,
}
var fetchSummaries []func(int64, *models.ProjectSummary)
if hasPerm, _ := p.HasProjectPermission(p.project.ProjectID, rbac.ActionRead, rbac.ResourceQuota); hasPerm {
fetchSummaries = append(fetchSummaries, getProjectQuotaSummary)
}
if hasPerm, _ := p.HasProjectPermission(p.project.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm {
fetchSummaries = append(fetchSummaries, getProjectMemberSummary)
}
var wg sync.WaitGroup
for _, fn := range []func(int64, *models.ProjectSummary){getProjectQuotaSummary, getProjectMemberSummary} {
for _, fn := range fetchSummaries {
fn := fn
wg.Add(1)

View File

@ -191,7 +191,7 @@ func (pma *ProjectMemberAPI) Put() {
pma.SendBadRequestError(err)
return
}
if req.Role < 1 || req.Role > 4 {
if !isValidRole(req.Role) {
pma.SendBadRequestError(fmt.Errorf("Invalid role id %v", req.Role))
return
}
@ -284,9 +284,22 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) {
return 0, ErrDuplicateProjectMember
}
if member.Role < 1 || member.Role > 4 {
if !isValidRole(member.Role) {
// Return invalid role error
return 0, ErrInvalidRole
}
return project.AddProjectMember(member)
}
func isValidRole(role int) bool {
switch role {
case common.RoleProjectAdmin,
common.RoleMaster,
common.RoleDeveloper,
common.RoleGuest,
common.RoleLimitedGuest:
return true
default:
return false
}
}

View File

@ -344,3 +344,28 @@ func TestProjectMemberAPI_PutAndDelete(t *testing.T) {
runCodeCheckingCases(t, cases...)
}
func Test_isValidRole(t *testing.T) {
type args struct {
role int
}
tests := []struct {
name string
args args
want bool
}{
{"project admin", args{1}, true},
{"master", args{4}, true},
{"developer", args{2}, true},
{"guest", args{3}, true},
{"limited guest", args{5}, true},
{"unknow", args{6}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isValidRole(tt.args.role); got != tt.want {
t.Errorf("isValidRole() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -44,6 +44,8 @@ import (
"github.com/goharbor/harbor/src/core/config"
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
coreutils "github.com/goharbor/harbor/src/core/utils"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model"
@ -729,6 +731,9 @@ func assembleTag(c chan *models.TagResp, client *registry.Repository, projectID
}
}
// get immutable status
item.Immutable = isImmutable(projectID, repository, tag)
c <- item
}
@ -809,6 +814,21 @@ func populateAuthor(detail *models.TagDetail) {
}
}
// check whether the tag is immutable
func isImmutable(projectID int64, repo string, tag string) bool {
_, repoName := utils.ParseRepository(repo)
matched, err := rule.NewRuleMatcher(projectID).Match(art.Candidate{
Repository: repoName,
Tag: tag,
NamespaceID: projectID,
})
if err != nil {
log.Error(err)
return false
}
return matched
}
// GetManifests returns the manifest of a tag
func (ra *RepositoryAPI) GetManifests() {
repoName := ra.GetString(":splat")
@ -954,7 +974,7 @@ func (ra *RepositoryAPI) Put() {
}
if !ra.SecurityCtx.IsAuthenticated() {
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
ra.SendUnAuthorizedError(errors.New("unauthorized"))
return
}

View File

@ -115,6 +115,7 @@ func TestSysCVEWhitelistAPIPut(t *testing.T) {
ExpiresAt: &s,
Items: []models.CVEWhitelistItem{
{CVEID: "CVE-2019-12310"},
{CVEID: "RHSA-2019:2237"},
},
},
credential: sysAdmin,

View File

@ -455,26 +455,12 @@ func (s *sessionReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
log.Info("can not get user information from session")
return false
}
if ctx.Request.Context().Value(AuthModeKey).(string) == common.OIDCAuth {
ou, err := dao.GetOIDCUserByUserID(user.UserID)
if err != nil {
log.Errorf("Failed to get OIDC user info, error: %v", err)
return false
}
if ou != nil { // If user does not have OIDC metadata, it means he is not onboarded via OIDC authn,
// so we can skip checking the token.
if err := oidc.VerifyAndPersistToken(ctx.Request.Context(), ou); err != nil {
log.Errorf("Failed to verify token, error: %v", err)
return false
}
}
}
log.Debug("using local database project manager")
pm := config.GlobalProjectMgr
log.Debug("creating local database security context...")
securCtx := local.NewSecurityContext(&user, pm)
securityCtx := local.NewSecurityContext(&user, pm)
setSecurCtxAndPM(ctx.Request, securCtx, pm)
setSecurCtxAndPM(ctx.Request, securityCtx, pm)
return true
}

View File

@ -35,4 +35,4 @@ var ChartMiddlewares = []string{CHART}
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
// MiddlewaresLocal ...
var MiddlewaresLocal = []string{SIZEQUOTA, COUNTQUOTA}
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}

View File

@ -0,0 +1,54 @@
package immutable
import (
"fmt"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/interceptor/immutable"
"github.com/goharbor/harbor/src/core/middlewares/util"
"net/http"
)
var (
defaultBuilders = []interceptor.Builder{
&manifestDeletionBuilder{},
&manifestCreationBuilder{},
}
)
type manifestDeletionBuilder struct{}
func (*manifestDeletionBuilder) Build(req *http.Request) (interceptor.Interceptor, error) {
if match, _, _ := util.MatchDeleteManifest(req); !match {
return nil, nil
}
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok {
var err error
info, err = util.ParseManifestInfoFromPath(req)
if err != nil {
return nil, fmt.Errorf("failed to parse manifest, error %v", err)
}
}
return immutable.NewDeleteMFInteceptor(info), nil
}
type manifestCreationBuilder struct{}
func (*manifestCreationBuilder) Build(req *http.Request) (interceptor.Interceptor, error) {
if match, _, _ := util.MatchPushManifest(req); !match {
return nil, nil
}
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok {
var err error
info, err = util.ParseManifestInfoFromReq(req)
if err != nil {
return nil, fmt.Errorf("failed to parse manifest, error %v", err)
}
}
return immutable.NewPushMFInteceptor(info), nil
}

View File

@ -16,78 +16,74 @@ package immutable
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
common_util "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/util"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
"net/http"
)
type immutableHandler struct {
next http.Handler
builders []interceptor.Builder
next http.Handler
}
// New ...
func New(next http.Handler) http.Handler {
func New(next http.Handler, builders ...interceptor.Builder) http.Handler {
if len(builders) == 0 {
builders = defaultBuilders
}
return &immutableHandler{
next: next,
builders: builders,
next: next,
}
}
// ServeHTTP ...
func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if match, _, _ := util.MatchPushManifest(req); !match {
func (rh *immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
interceptor, err := rh.getInterceptor(req)
if err != nil {
log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)),
http.StatusInternalServerError)
return
}
if interceptor == nil {
rh.next.ServeHTTP(rw, req)
return
}
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok {
var err error
info, err = util.ParseManifestInfoFromPath(req)
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
if err := interceptor.HandleRequest(req); err != nil {
log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
if _, ok := err.(middlerware_err.ErrImmutable); ok {
http.Error(rw, util.MarshalError("DENIED",
fmt.Sprintf("The tag is immutable, cannot be overwrite: %v", err)), http.StatusPreconditionFailed)
return
}
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)),
http.StatusInternalServerError)
return
}
rh.next.ServeHTTP(rw, req)
interceptor.HandleResponse(rw, req)
}
func (rh *immutableHandler) getInterceptor(req *http.Request) (interceptor.Interceptor, error) {
for _, builder := range rh.builders {
interceptor, err := builder.Build(req)
if err != nil {
return nil, err
}
if interceptor != nil {
return interceptor, nil
}
}
_, repoName := common_util.ParseRepository(info.Repository)
matched, err := rule.NewRuleMatcher(info.ProjectID).Match(art.Candidate{
Repository: repoName,
Tag: info.Tag,
NamespaceID: info.ProjectID,
})
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
return
}
if !matched {
rh.next.ServeHTTP(rw, req)
return
}
artifactQuery := &models.ArtifactQuery{
PID: info.ProjectID,
Repo: info.Repository,
Tag: info.Tag,
}
afs, err := dao.ListArtifacts(artifactQuery)
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
return
}
if len(afs) == 0 {
rh.next.ServeHTTP(rw, req)
return
}
// rule matched and non-existent is a immutable tag
http.Error(rw, util.MarshalError("DENIED",
fmt.Sprintf("The tag:%s:%s is immutable, cannot be overwrite.", info.Repository, info.Tag)), http.StatusPreconditionFailed)
return
return nil, nil
}

View File

@ -0,0 +1,65 @@
package immutable
import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
common_util "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/util"
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"net/http"
)
// NewDeleteMFInteceptor ....
func NewDeleteMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor {
return &delmfInterceptor{
mf: mf,
}
}
type delmfInterceptor struct {
mf *util.ManifestInfo
}
// HandleRequest ...
func (dmf *delmfInterceptor) HandleRequest(req *http.Request) (err error) {
artifactQuery := &models.ArtifactQuery{
Digest: dmf.mf.Digest,
}
var afs []*models.Artifact
afs, err = dao.ListArtifacts(artifactQuery)
if err != nil {
log.Error(err)
return
}
if len(afs) == 0 {
return
}
for _, af := range afs {
_, repoName := common_util.ParseRepository(dmf.mf.Repository)
var matched bool
matched, err = rule.NewRuleMatcher(dmf.mf.ProjectID).Match(art.Candidate{
Repository: repoName,
Tag: af.Tag,
NamespaceID: dmf.mf.ProjectID,
})
if err != nil {
log.Error(err)
return
}
if matched {
return middlerware_err.NewErrImmutable(repoName)
}
}
return
}
// HandleRequest ...
func (dmf *delmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) {
}

View File

@ -0,0 +1,65 @@
package immutable
import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
common_util "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/util"
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"net/http"
)
// NewPushMFInteceptor ....
func NewPushMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor {
return &pushmfInterceptor{
mf: mf,
}
}
type pushmfInterceptor struct {
mf *util.ManifestInfo
}
// HandleRequest ...
func (pmf *pushmfInterceptor) HandleRequest(req *http.Request) (err error) {
_, repoName := common_util.ParseRepository(pmf.mf.Repository)
var matched bool
matched, err = rule.NewRuleMatcher(pmf.mf.ProjectID).Match(art.Candidate{
Repository: repoName,
Tag: pmf.mf.Tag,
NamespaceID: pmf.mf.ProjectID,
})
if err != nil {
log.Error(err)
return
}
if !matched {
return
}
artifactQuery := &models.ArtifactQuery{
PID: pmf.mf.ProjectID,
Repo: pmf.mf.Repository,
Tag: pmf.mf.Tag,
}
var afs []*models.Artifact
afs, err = dao.ListArtifacts(artifactQuery)
if err != nil {
log.Error(err)
return
}
if len(afs) == 0 {
return
}
return middlerware_err.NewErrImmutable(repoName)
}
// HandleRequest ...
func (pmf *pushmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) {
}

View File

@ -0,0 +1,20 @@
package error
import (
"fmt"
)
// ErrImmutable ...
type ErrImmutable struct {
repo string
}
// Error ...
func (ei ErrImmutable) Error() string {
return fmt.Sprintf("Failed to process request, due to immutable. '%s'", ei.repo)
}
// NewErrImmutable ...
func NewErrImmutable(msg string) ErrImmutable {
return ErrImmutable{repo: msg}
}

View File

@ -17,7 +17,6 @@ package whitelist
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
"regexp"
)
type invalidErr struct {
@ -46,11 +45,12 @@ 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)
// re := regexp.MustCompile(cveIDPattern)
for _, it := range wl.Items {
if !re.MatchString(it.CVEID) {
return &invalidErr{fmt.Sprintf("invalid CVE ID: %s", it.CVEID)}
}
// Bypass the cve format checking
// 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)}
}

View File

@ -67,6 +67,7 @@ func TestValidate(t *testing.T) {
l: models.CVEWhitelist{
Items: []models.CVEWhitelistItem{
{CVEID: "breakit"},
{CVEID: "breakit"},
},
},
noError: false,

103
src/portal/lib/src/cache/index.ts vendored Normal file
View File

@ -0,0 +1,103 @@
// 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.
import { Observable, of } from "rxjs";
import { tap, publishReplay, refCount } from "rxjs/operators";
function hashCode(str: string): string {
let hash: number = 0;
let chr: number;
if (str.length === 0) {
return hash.toString(36);
}
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
/* tslint:disable:no-bitwise */
hash = ((hash << 5) - hash) + chr;
hash |= 0;
/* tslint:enable:no-bitwise */
}
return hash.toString(36);
}
interface IObservableCacheValue {
response: Observable<any>;
/**
* created time of the cache value
*/
created?: Date;
}
interface IObservableCacheConfig {
/**
* maxAge of cache in milliseconds
*/
maxAge?: number;
/**
* whether should use sliding expiration on caches
*/
slidingExpiration?: boolean;
}
const cache: Map<string, IObservableCacheValue> = new Map<string, IObservableCacheValue>();
export function CacheObservable(config: IObservableCacheConfig = {}) {
return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
const targetName = target.constructor.name;
(descriptor.value as any) = function (...args: Array<any>) {
const key = hashCode(`${targetName}:${methodName}:${JSON.stringify(args)}`);
let value = cache.get(key);
if (value && value.created) {
if (new Date().getTime() - new Date(value.created).getTime() > config.maxAge) {
cache[key] = null;
value = null;
} else if (config.slidingExpiration) {
value.created = new Date();
cache.set(key, value);
}
}
if (value) {
return of(value.response);
}
const response$ = (original.apply(this, args) as Observable<any>).pipe(
tap((response: Observable<any>) => {
cache.set(key, {
response: response,
created: config.maxAge ? new Date() : null
});
}),
publishReplay(1),
refCount()
);
return response$;
};
return descriptor;
};
}
export function FlushAll() {
cache.clear();
}

View File

@ -10,7 +10,7 @@
<clr-tab>
<button id="config-vulnerability" clrTabLink>{{'CONFIG.VULNERABILITY' | translate}}</button>
<clr-tab-content id="vulnerability" *clrIfActive>
<vulnerability-config *ngIf="withClair" #vulnerabilityConfig [showSubTitle]="true"></vulnerability-config>
<vulnerability-config #vulnerabilityConfig [showSubTitle]="true"></vulnerability-config>
</clr-tab-content>
</clr-tab>
<clr-tab>
@ -21,4 +21,4 @@
</clr-tab-content>
</clr-tab>
</clr-tabs>
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>

View File

@ -18,4 +18,7 @@
.replication-tooltip {
top: -8px;
}
}
.margin-top-3px {
margin-top: 3px;
}

View File

@ -73,7 +73,7 @@
</clr-checkbox-container>
<div class="clr-form-control d-f" *ngIf="withClair">
<div class="clr-form-control d-f">
<label for="systemWhitelist"
class="clr-control-label">{{'CVE_WHITELIST.DEPLOYMENT_SECURITY'|translate}}</label>
<div class="form-content">
@ -172,4 +172,4 @@
[disabled]="(!isValid() || !hasChanges()) && (!hasWhitelistChanged) || inProgress">{{'BUTTON.CANCEL'
| translate}}</button>
</div>
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>

View File

@ -118,7 +118,7 @@
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='name'">{{'TOOLTIP.NAME_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='tag'">{{'TOOLTIP.TAG_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='label'">{{'TOOLTIP.LABEL_FILTER' | translate}}</span>
<span class="tooltip-content" >{{'TOOLTIP.LABEL_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='resource'">{{'TOOLTIP.RESOURCE_FILTER' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
@ -209,7 +209,7 @@
</clr-tooltip>
</label>
</div>
<div class="clr-checkbox-wrapper clr-form-control">
<div class="clr-checkbox-wrapper clr-form-control" [hidden]="policyId < 0">
<input type="checkbox" [checked]="true" id="enablePolicy" formControlName="enabled" class="clr-checkbox">
<label for="enablePolicy" class="clr-control-label">{{'REPLICATION.ENABLED_RULE' | translate}}</label>
</div>

View File

@ -203,8 +203,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
})
}),
filters: this.fb.array([]),
deletion: false,
enabled: true,
deletion: false,
override: true
});
}
@ -253,7 +253,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
dest_registry: rule.dest_registry,
trigger: rule.trigger,
deletion: rule.deletion,
enabled: rule.enabled,
enabled: true,
override: rule.override
});
let filtersArray = this.getFilterArray(rule);

View File

@ -29,4 +29,5 @@ export * from "./repository-gridview/index";
export * from "./operation/index";
export * from "./_animations/index";
export * from "./cron-schedule/index";
export * from "./cache/index";

View File

@ -10,7 +10,7 @@
<clr-control-helper class="config-subtext"> {{ 'PROJECT_CONFIG.PUBLIC_POLICY' | translate }}
</clr-control-helper>
</clr-checkbox-container>
<clr-checkbox-container *ngIf="withNotary || withClair">
<clr-checkbox-container *ngIf="withNotary">
<label><span *ngIf="withNotary">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</span></label>
<clr-checkbox-wrapper *ngIf="withNotary">
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ContentTrust" name="content-trust"
@ -20,9 +20,9 @@
<clr-control-helper class="config-subtext"> {{ 'PROJECT_CONFIG.CONTENT_TRUST_POLCIY' | translate }}
</clr-control-helper>
</clr-checkbox-container>
<clr-checkbox-container id="prevent-vulenrability-image" *ngIf="withNotary || withClair">
<clr-checkbox-container id="prevent-vulenrability-image" *ngIf="withNotary">
<label><span *ngIf="!withNotary">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</span></label>
<clr-checkbox-wrapper *ngIf="withClair">
<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>
@ -46,7 +46,7 @@
</div>
</clr-control-helper>
</clr-checkbox-container>
<clr-checkbox-container *ngIf="withClair">
<clr-checkbox-container>
<label>{{ 'PROJECT_CONFIG.SCAN' | translate }}</label>
<clr-checkbox-wrapper id="scan-image-on-push-wrapper">
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ScanImgOnPush"
@ -56,7 +56,7 @@
<clr-control-helper class="config-subtext"> {{ 'PROJECT_CONFIG.AUTOSCAN_POLICY' | translate }}
</clr-control-helper>
</clr-checkbox-container>
<div class="clr-form-control" [class.clr-form-control-disabled]="!hasChangeConfigRole" *ngIf="withClair">
<div class="clr-form-control" [class.clr-form-control-disabled]="!hasChangeConfigRole">
<label for="systemWhitelist" class="clr-control-label">{{'CVE_WHITELIST.CVE_WHITELIST'|translate}}</label>
<div class="w-100 clr-control-container">
<div class="config-subtext">

View File

@ -55,10 +55,9 @@
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
<div id=images-container>
<hbr-tag ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)"
class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary"
[withClair]="withClair" [withAdmiral]="withAdmiral" [hasSignedIn]="hasSignedIn"
class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [withAdmiral]="withAdmiral" [hasSignedIn]="hasSignedIn"
[isGuest]="isGuest" [projectId]="projectId" [memberRoleID]="memberRoleID"></hbr-tag>
</div>
</section>
</div>
</section>
</section>

View File

@ -68,6 +68,7 @@ export interface Tag extends Base {
labels: Label[];
push_time?: string;
pull_time?: string;
immutable?: boolean;
}
/**

View File

@ -53,6 +53,12 @@ export const USERSTATICPERMISSION = {
"READ": "read",
}
},
"QUOTA": {
"KEY": "quota",
"VALUE": {
"READ": "read"
}
},
"REPOSITORY": {
'KEY': 'repository',
'VALUE': {

View File

@ -13,14 +13,16 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Observable, throwError as observableThrowError } from "rxjs";
import { map, catchError, shareReplay } from "rxjs/operators";
import { UserPrivilegeServeItem } from './interface';
import { Observable, forkJoin, of, throwError as observableThrowError } from "rxjs";
import { map, tap, publishReplay, refCount } from "rxjs/operators";
import { HttpClient } from '@angular/common/http';
import { CacheObservable } from '../cache';
interface Permission {
resource: string;
action: string;
}
const CACHE_SIZE = 1;
/**
* Get System privilege about current backend server.
* @abstract
@ -35,8 +37,11 @@ export abstract class UserPermissionService {
*/
abstract getPermission(projectId, resource, action);
abstract clearPermissionCache();
abstract hasProjectPermission(projectId: any, permission: Permission): Observable<boolean>;
abstract hasProjectPermissions(projectId: any, permissions: Array<Permission>): Observable<Array<boolean>>;
}
// @dynamic
@Injectable()
export class UserPermissionDefaultService extends UserPermissionService {
constructor(
@ -44,36 +49,37 @@ export class UserPermissionDefaultService extends UserPermissionService {
) {
super();
}
private permissionCache: Observable<object>;
private projectId: number;
private getPermissionFromBackend(projectId): Observable<object> {
const userPermissionUrl = `/api/users/current/permissions?scope=/project/${projectId}&relative=true`;
return this.http.get(userPermissionUrl);
}
private processingPermissionResult(responsePermission, resource, action): boolean {
const permissionList = responsePermission as UserPrivilegeServeItem[];
for (const privilegeItem of permissionList) {
if (privilegeItem.resource === resource && privilegeItem.action === action) {
return true;
}
}
return false;
}
public getPermission(projectId, resource, action): Observable<boolean> {
if (!this.permissionCache || this.projectId !== +projectId) {
this.projectId = +projectId;
this.permissionCache = this.getPermissionFromBackend(projectId).pipe(
shareReplay(CACHE_SIZE));
}
return this.permissionCache.pipe(map(response => {
return this.processingPermissionResult(response, resource, action);
}))
.pipe(catchError(error => observableThrowError(error)
@CacheObservable({ maxAge: 1000 * 60 })
private getPermissions(scope: string, relative?: boolean): Observable<Array<Permission>> {
const url = `/api/users/current/permissions?scope=${scope}&relative=${relative ? 'true' : 'false'}`;
return this.http.get<Array<Permission>>(url);
}
private hasPermission(permission: Permission, scope: string, relative?: boolean): Observable<boolean> {
return this.getPermissions(scope, relative).pipe(map(
(permissions: Array<Permission>) => {
return permissions.some((p: Permission) => p.resource === permission.resource && p.action === permission.action);
}
));
}
private hasPermissions(permissions: Array<Permission>, scope: string, relative?: boolean): Observable<Array<boolean>> {
return forkJoin(permissions.map((permission) => this.hasPermission(permission, scope, relative)));
}
public hasProjectPermission(projectId: any, permission: Permission): Observable<boolean> {
return this.hasPermission(permission, `/project/${projectId}`, true);
}
public hasProjectPermissions(projectId: any, permissions: Array<Permission>): Observable<Array<boolean>> {
return this.hasPermissions(permissions, `/project/${projectId}`, true);
}
public getPermission(projectId: any, resource: string, action: string): Observable<boolean> {
return this.hasProjectPermission(projectId, { resource, action });
}
public clearPermissionCache() {
this.permissionCache = null;
this.projectId = null;
}
}

View File

@ -145,6 +145,11 @@ export const PROJECT_ROOTS = [
NAME: "guest",
VALUE: 3,
LABEL: "GROUP.GUEST"
},
{
NAME: "limited",
VALUE: 5,
LABEL: "GROUP.LIMITED_GUEST"
}
];

View File

@ -85,7 +85,7 @@
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="!withAdmiral">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="pushComparator">{{'REPOSITORY.PUSH_TIME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="pullComparator">{{'REPOSITORY.PULL_TIME' | translate}}</clr-dg-column>
@ -93,6 +93,7 @@
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated flex-max-width">
<a href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngIf="t.immutable" class="label label-info ml-1">{{'REPOSITORY.IMMUTABLE' | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{sizeTransform(t.size)}}</clr-dg-cell>
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
@ -111,7 +112,7 @@
</clr-dg-cell>
<clr-dg-cell class="truncated" title="{{t.author}}">{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell *ngIf="!withAdmiral">
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]" [labelWidth]="90"> </hbr-label-piece>
<div class="signpost-item" [hidden]="t.labels?.length<=1">
@ -136,4 +137,4 @@
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
</div>

View File

@ -723,9 +723,7 @@ export class TagComponent implements OnInit, AfterViewInit {
canScanNow(t: Tag[]): boolean {
if (!this.hasScanImagePermission) { return false; }
let st: string = this.scanStatus(t[0]);
return st !== VULNERABILITY_SCAN_STATUS.PENDING &&
st !== VULNERABILITY_SCAN_STATUS.RUNNING;
return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
}
getImagePermissionRule(projectId: number): void {
let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY,

View File

@ -19,7 +19,6 @@ clr-modal {
align-items: center;
.reset-cli {
height: 30px;
padding-top: 8px;
}
.btn-padding-less {
padding-left: 5px;

View File

@ -374,6 +374,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
}
closeReset() {
this.showSecretDetail = false;
this.showGenerateCliFn();
this.resetSecretFrom.resetForm(new ResetSecret());
}
}

View File

@ -43,12 +43,12 @@
</a>
</clr-vertical-nav-group-children>
</clr-vertical-nav-group>
<clr-vertical-nav-group *ngIf="isSystemAdmin && (withClair || hasAdminRole)" routerLinkActive="active">
<clr-vertical-nav-group *ngIf="isSystemAdmin && 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"
<a clrVerticalNavLink routerLink="/harbor/vulnerability"
routerLinkActive="active">
{{'SIDE_NAV.SYSTEM_MGMT.VULNERABILITY' | translate}}
</a>

View File

@ -17,18 +17,26 @@
<button clrDropdownItem
(click)="changeStat()"
[disabled]="!(selectedRow && !selectedRow.is_default)">
<span *ngIf="selectedRow && selectedRow.disabled">{{'BUTTON.ENABLE' | translate}}</span>
<span *ngIf="!(selectedRow && selectedRow.disabled)">{{'BUTTON.DISABLE' | translate}}</span>
<span *ngIf="selectedRow && selectedRow.disabled">
<clr-icon class="margin-top-2" size="16" shape="success-standard"></clr-icon>
<span class="margin-left-10">{{'SCANNER.ENABLE' | translate}}</span>
</span>
<span *ngIf="!(selectedRow && selectedRow.disabled)">
<clr-icon class="margin-top-2" size="16" shape="ban"></clr-icon>
<span class="margin-left-10">{{'SCANNER.DISABLE' | translate}}</span>
</span>
</button>
<button clrDropdownItem
(click)="editScanner()"
class="btn btn-sm btn-secondary" [disabled]="!selectedRow">
{{'BUTTON.EDIT' | translate}}
<clr-icon class="margin-top-0" size="16" shape="pencil"></clr-icon>
<span class="margin-left-10">{{'BUTTON.EDIT' | translate}}</span>
</button>
<button clrDropdownItem
(click)="deleteScanners()"
class="btn btn-sm btn-secondary" [disabled]="!selectedRow">
{{'BUTTON.DELETE' | translate}}
<clr-icon class="margin-top-0" size="16" shape="times"></clr-icon>
<span class="margin-left-10">{{'BUTTON.DELETE' | translate}}</span>
</button>
</clr-dropdown-menu>
</clr-dropdown>
@ -45,7 +53,7 @@
<clr-dg-column class="width-240" [clrDgField]="'name'">{{'SCANNER.NAME' | translate}}</clr-dg-column>
<clr-dg-column class="width-240" [clrDgField]="'url'">{{'SCANNER.ENDPOINT' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.HEALTH' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.DISABLED' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.ENABLED' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.AUTH' | translate}}</clr-dg-column>
<clr-dg-placeholder>
{{'SCANNER.NO_SCANNER' | translate}}
@ -62,7 +70,7 @@
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</clr-dg-cell>
<clr-dg-cell>{{scanner.disabled}}</clr-dg-cell>
<clr-dg-cell>{{!scanner.disabled}}</clr-dg-cell>
<clr-dg-cell>{{scanner.auth?scanner.auth:'None'}}</clr-dg-cell>
<scanner-metadata *clrIfExpanded [uid]="scanner.uuid" ngProjectAs="clr-dg-row-detail"></scanner-metadata>
</clr-dg-row>

View File

@ -22,3 +22,12 @@
.width-240 {
min-width: 240px !important;
}
.margin-top-0 {
margin-top: 0;
}
.margin-top-2 {
margin-top: 2px;
}
.margin-left-10 {
margin-left: 10px;
}

View File

@ -35,7 +35,7 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
confirmed.state === ConfirmationState.CONFIRMED) {
this.configScannerService.deleteScanners(confirmed.data)
.subscribe(response => {
this.msgHandler.showSuccess("Delete Success");
this.msgHandler.showSuccess("SCANNER.DELETE_SUCCESS");
this.getScanners();
}, error => {
this.errorHandler.error(error);
@ -76,7 +76,7 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
scanner.disabled = !scanner.disabled;
this.configScannerService.updateScanner(scanner)
.subscribe(response => {
this.msgHandler.showSuccess("Update Success");
this.msgHandler.showSuccess("SCANNER.UPDATE_SUCCESS");
this.getScanners();
}, error => {
this.errorHandler.error(error);
@ -87,7 +87,7 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
if (this.selectedRow) {
this.configScannerService.setAsDefault(this.selectedRow.uuid)
.subscribe(response => {
this.msgHandler.showSuccess("Update Success");
this.msgHandler.showSuccess("SCANNER.UPDATE_SUCCESS");
this.getScanners();
}, error => {
this.errorHandler.error(error);

View File

@ -4,7 +4,7 @@
<label class="required clr-control-label">{{"SCANNER.NAME" | translate}}</label>
<div class="clr-control-container" [class.clr-error]="!isNameValid">
<div class="clr-input-wrapper">
<input autocomplete="off" #name formControlName="name" class="clr-input width-312"
<input autocomplete="off" #name formControlName="name" class="clr-input width-280"
type="text"
id="scanner-name">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
@ -18,7 +18,7 @@
<div class="clr-form-control">
<label class="clr-control-label">{{"SCANNER.DESCRIPTION" | translate}}</label>
<div class="clr-control-container">
<textarea autocomplete="off" formControlName="description" class="clr-textarea width-312" type="text"
<textarea autocomplete="off" formControlName="description" class="clr-textarea width-280" type="text"
id="description">
</textarea>
</div>
@ -28,7 +28,7 @@
<div class="clr-control-container" [class.clr-error]="!isEndpointValid || showEndpointError">
<div class="clr-input-wrapper">
<input (focus)="showEndpointError=false" (blur)="checkEndpointUrl()" #endpointUrl placeholder="http(s)://192.168.1.1" autocomplete="off" formControlName="url"
class="clr-input width-312" type="text" id="scanner-endpoint">
class="clr-input width-280" type="text" id="scanner-endpoint">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkEndpointOnGoing"></span>
</div>
@ -41,7 +41,7 @@
<label class="clr-control-label">{{"SCANNER.AUTH" | translate}}</label>
<div class="clr-control-container">
<div class="clr-select-wrapper">
<select formControlName="auth" class="clr-select width-312" id="scanner-authorization">
<select formControlName="auth" class="clr-select width-280" id="scanner-authorization">
<option value="None">{{"SCANNER.NONE" | translate}}</option>
<option value="Basic">{{"SCANNER.BASIC" | translate}}</option>
<option value="Bearer">{{"SCANNER.BEARER" | translate}}</option>
@ -56,7 +56,7 @@
<div class="clr-control-container" [class.clr-error]="!isUserNameValid">
<div class="clr-input-wrapper">
<input formControlName="username" autocomplete="off"
class="clr-input width-312" type="text" id="scanner-username">
class="clr-input width-280" type="text" id="scanner-username">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="!isUserNameValid">
@ -69,7 +69,7 @@
<div class="clr-control-container" [class.clr-error]="!isPasswordValid">
<div class="clr-input-wrapper">
<input formControlName="password" autocomplete="off"
class="clr-input width-312" type="password" id="scanner-password">
class="clr-input width-280" type="password" id="scanner-password">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="!isPasswordValid">
@ -82,7 +82,7 @@
<div class="clr-control-container" [class.clr-error]="!isTokenValid">
<div class="clr-input-wrapper">
<input formControlName="token" autocomplete="off"
class="clr-input width-312" type="text" id="scanner-token">
class="clr-input width-280" type="text" id="scanner-token">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="!isTokenValid">
@ -95,7 +95,7 @@
<div class="clr-control-container" [class.clr-error]="!isApiKeyValid">
<div class="clr-input-wrapper">
<input formControlName="apiKey" autocomplete="off"
class="clr-input width-312" type="text" id="scanner-apiKey">
class="clr-input width-280" type="text" id="scanner-apiKey">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="!isApiKeyValid">

View File

@ -1,6 +1,9 @@
.width-312 {
width: 312px;
.width-280 {
width: 280px;
}
.padding-top-3 {
padding-top: 3px;
}
.clr-control-label {
width: 9rem !important;
}

View File

@ -21,7 +21,7 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
checkOnGoing: boolean = false;
newScannerForm: FormGroup = this.fb.group({
name: this.fb.control("",
[Validators.required, Validators.pattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/)]),
[Validators.required]),
description: this.fb.control(""),
url: this.fb.control("",
[Validators.required,
@ -57,7 +57,7 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
if (this.isEdit && this.originValue && this.originValue.name === name) {
return false;
}
return this.newScannerForm.get('name').valid && name.length > 1;
return this.newScannerForm.get('name').valid && name.length > 0;
}),
debounceTime(500),
distinctUntilChanged(),
@ -130,15 +130,15 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
return true;
}
if (this.isNameExisting) {
this.nameTooltip = 'NAME_EXISTS';
this.nameTooltip = 'SCANNER.NAME_EXISTS';
return false;
}
if (this.newScannerForm.get('name').errors && this.newScannerForm.get('name').errors.required) {
this.nameTooltip = 'NAME_REQUIRED';
this.nameTooltip = 'SCANNER.NAME_REQUIRED';
return false;
}
if (this.newScannerForm.get('name').errors && this.newScannerForm.get('name').errors.pattern) {
this.nameTooltip = 'NAME_REX';
this.nameTooltip = 'SCANNER.NAME_REX';
return false;
}
return true;
@ -151,11 +151,11 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
return true;
}
if (this.isEndpointUrlExisting) {
this.endpointTooltip = 'ENDPOINT_EXISTS';
this.endpointTooltip = 'SCANNER.ENDPOINT_EXISTS';
return false;
}
if (this.newScannerForm.get('url').errors && this.newScannerForm.get('url').errors.required) {
this.endpointTooltip = 'ENDPOINT_REQUIRED';
this.endpointTooltip = 'SCANNER.ENDPOINT_REQUIRED';
return false;
}
// skip here, validate when onblur
@ -167,7 +167,7 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
// validate endpointUrl when onblur
checkEndpointUrl() {
if (this.newScannerForm.get('url').errors && this.newScannerForm.get('url').errors.pattern) {
this.endpointTooltip = "ILLEGAL_ENDPOINT";
this.endpointTooltip = "SCANNER.ILLEGAL_ENDPOINT";
this.showEndpointError = true;
}
}

View File

@ -45,6 +45,10 @@
<input clrRadio type="radio" name="member_role" id="checkrads_guest" [value]=3 [(ngModel)]="member.role_id">
<label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" name="member_role" id="checkrads_limited_guest" [value]=5 [(ngModel)]="member.role_id">
<label for="checkrads_limited_guest">{{'MEMBER.LIMITED_GUEST' | translate}}</label>
</clr-radio-wrapper>
</div>
</div>
</form>

View File

@ -27,6 +27,7 @@
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 4)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.PROJECT_MASTER' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 2)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.DEVELOPER' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 3)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.GUEST' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 5)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.LIMITED_GUEST' | translate}}</button>
<div class="dropdown-divider"></div>
<button clrDropdownItem (click)="openDeleteMembersDialog(selectedRow)" [disabled]="!(selectedRow.length && hasDeleteMemberPermission) || onlySelf">{{'MEMBER.REMOVE' | translate}}</button>
</clr-dropdown-menu>

View File

@ -256,7 +256,7 @@ export class MemberComponent implements OnInit, OnDestroy {
// Function to delete specific member
let deleteMember = (projectId: number, member: Member) => {
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.DELETE_MEMBER';
operMessage.name = member.entity_type === 'u' ? 'OPERATION.DELETE_MEMBER' : 'OPERATION.DELETE_GROUP';
operMessage.data.id = member.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = member.entity_name;

View File

@ -79,7 +79,7 @@
[(ngModel)]="imagePermissionPull" clrCheckbox>
</td>
</tr>
<tr>
<tr *ngIf="withHelmChart">
<td class="left">{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</td>
<td>
<input type="checkbox"

View File

@ -8,6 +8,7 @@ import { of } from "rxjs";
import { ErrorHandler } from "@harbor/ui";
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AppConfigService } from "../../../app-config.service";
describe('AddRobotComponent', () => {
let component: AddRobotComponent;
@ -24,6 +25,13 @@ describe('AddRobotComponent', () => {
let fakeMessageHandlerService = {
showSuccess: function() {}
};
let fakeAppConfigService = {
getConfig: function() {
return {
with_chartmuseum: true
};
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -40,6 +48,7 @@ describe('AddRobotComponent', () => {
TranslateService,
ErrorHandler,
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: RobotService, useValue: fakeRobotService }
]
}).compileComponents();

View File

@ -18,6 +18,7 @@ import { ErrorHandler } from "@harbor/ui";
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { AppConfigService } from "../../../app-config.service";
@Component({
selector: "add-robot",
@ -43,6 +44,7 @@ export class AddRobotComponent implements OnInit, OnDestroy {
robotForm: NgForm;
imagePermissionPush: boolean = true;
imagePermissionPull: boolean = true;
withHelmChart: boolean;
@Input() projectId: number;
@Input() projectName: string;
@Output() create = new EventEmitter<boolean>();
@ -54,10 +56,13 @@ export class AddRobotComponent implements OnInit, OnDestroy {
private errorHandler: ErrorHandler,
private cdr: ChangeDetectorRef,
private messageHandlerService: MessageHandlerService,
private sanitizer: DomSanitizer
) {}
private sanitizer: DomSanitizer,
private appConfigService: AppConfigService
) {}
ngOnInit(): void {
this.withHelmChart = this.appConfigService.getConfig().with_chartmuseum;
this.robotNameChecker.pipe(debounceTime(800)).subscribe((name: string) => {
let cont = this.currentForm.controls["robot_name"];
if (cont) {

View File

@ -5,7 +5,7 @@
<div class="clr-form-control">
<label class="clr-control-label name">{{'SCANNER.SCANNER' | translate}}</label>
<div class="clr-control-container">
<button *ngIf="scanners && scanners.length > 0" id="edit-scanner" class="btn btn-link edit" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
<label *ngIf="!(scanners && scanners.length > 0)" class="name">{{'SCANNER.NOT_AVAILABLE' | translate}}</label>
</div>
</div>
@ -16,9 +16,10 @@
<div class="clr-input-wrapper">
<div class="clr-input-wrapper">
<span id="scanner-name" class="scanner-name">{{scanner?.name}}</span>
<button *ngIf="scanners && scanners.length > 0" id="edit-scanner" class="btn btn-primary btn-sm" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
<span *ngIf="scanner?.disabled" class="label label-warning ml-1">{{'SCANNER.DISABLED' | translate}}</span>
<span *ngIf="scanner?.health" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
<span *ngIf="!scanner?.health" class="label label-danger ml-1">{{'SCANNER.Unhealthy' | translate}}</span>
<span *ngIf="!scanner?.health" class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</span>
</div>
</div>
</div>

View File

@ -18,3 +18,6 @@
justify-content: center;
align-items: center;
}
.clr-form-control {
margin-top: 0.75rem !important;
}

View File

@ -12,7 +12,7 @@
<li>{{summaryInformation?.chart_count}}</li>
</ul>
</div>
<div class="display-flex project-detail pt-1">
<div *ngIf="showProjectMemberInfo" class="display-flex project-detail pt-1">
<h5 class="mt-0">{{'SUMMARY.PROJECT_MEMBER' | translate}}</h5>
<ul class="list-unstyled">
<li>{{ summaryInformation?.project_admin_count }} {{'SUMMARY.ADMIN' | translate}}</li>
@ -22,7 +22,7 @@
</ul>
</div>
</div>
<div class="summary-right pt-1">
<div *ngIf="showQuotaInfo" class="summary-right pt-1">
<div class="display-flex project-detail">
<h5 class="mt-0">{{'SUMMARY.PROJECT_QUOTAS' | translate}}</h5>
<div class="ml-1">

View File

@ -2,12 +2,13 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClarityModule } from '@clr/angular';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ProjectService, ErrorHandler} from '@harbor/ui';
import { ProjectService, ErrorHandler, UserPermissionService } from '@harbor/ui';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { AppConfigService } from "../../app-config.service";
import { SummaryComponent } from './summary.component';
describe('SummaryComponent', () => {
let component: SummaryComponent;
let fixture: ComponentFixture<SummaryComponent>;
@ -18,6 +19,11 @@ describe('SummaryComponent', () => {
}
};
let fakeErrorHandler = null;
let fakeUserPermissionService = {
hasProjectPermissions: function() {
return of([true, true]);
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -34,6 +40,7 @@ describe('SummaryComponent', () => {
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: ProjectService, useValue: fakeProjectService },
{ provide: ErrorHandler, useValue: fakeErrorHandler },
{ provide: UserPermissionService, useValue: fakeUserPermissionService },
{
provide: ActivatedRoute, useValue: {
paramMap: of({ get: (key) => 'value' }),

View File

@ -1,41 +1,70 @@
import { Component, OnInit } from '@angular/core';
import { ProjectService, clone, QuotaUnits, getSuitableUnit, ErrorHandler, GetIntegerAndUnit
, QUOTA_DANGER_COEFFICIENT, QUOTA_WARNING_COEFFICIENT } from '@harbor/ui';
import { Component, Input, OnInit } from '@angular/core';
import {
ProjectService,
clone,
QuotaUnits,
getSuitableUnit,
ErrorHandler,
GetIntegerAndUnit,
UserPermissionService,
USERSTATICPERMISSION,
QUOTA_DANGER_COEFFICIENT,
QUOTA_WARNING_COEFFICIENT
} from '@harbor/ui';
import { ActivatedRoute } from '@angular/router';
import { AppConfigService } from "../../app-config.service";
@Component({
selector: 'summary',
templateUrl: './summary.component.html',
styleUrls: ['./summary.component.scss']
})
export class SummaryComponent implements OnInit {
showProjectMemberInfo: boolean;
showQuotaInfo: boolean;
projectId: number;
summaryInformation: any;
quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT;
quotaWarningCoefficient: number = QUOTA_WARNING_COEFFICIENT;
constructor(
private projectService: ProjectService,
private userPermissionService: UserPermissionService,
private errorHandler: ErrorHandler,
private appConfigService: AppConfigService,
private route: ActivatedRoute
) { }
) { }
ngOnInit() {
this.projectId = this.route.snapshot.parent.params['id'];
const permissions = [
{ resource: USERSTATICPERMISSION.MEMBER.KEY, action: USERSTATICPERMISSION.MEMBER.VALUE.LIST },
{ resource: USERSTATICPERMISSION.QUOTA.KEY, action: USERSTATICPERMISSION.QUOTA.VALUE.READ },
];
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
this.showProjectMemberInfo = results[0];
this.showQuotaInfo = results[1];
});
this.projectService.getProjectSummary(this.projectId).subscribe(res => {
this.summaryInformation = res;
}, error => {
this.errorHandler.error(error);
});
}
getSuitableUnit(value) {
const QuotaUnitsCopy = clone(QuotaUnits);
return getSuitableUnit(value, QuotaUnitsCopy);
}
getIntegerAndUnit(hardValue, usedValue) {
return GetIntegerAndUnit(hardValue, clone(QuotaUnits), usedValue, clone(QuotaUnits));
}
public get withHelmChart(): boolean {
return this.appConfigService.getConfig().with_chartmuseum;
}

View File

@ -6,8 +6,7 @@
</div>
<hbr-tag-detail (backEvt)="goBack($event)"
[tagId]="tagId"
[withClair]="withClair"
[withAdmiral]="withAdmiral"
[projectId]="projectId"
[repositoryId]="repositoryId"></hbr-tag-detail>
</div>
</div>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ErrorHandler, UserPermissionService, httpStatusCode, errorHandler } from '@harbor/ui';
import { ErrorHandler, httpStatusCode, errorHandler } from '@harbor/ui';
import { AlertType } from '../../shared/shared.const';
import { MessageService } from '../../global-message/message.service';
@ -26,7 +26,6 @@ export class MessageHandlerService implements ErrorHandler {
constructor(
private msgService: MessageService,
private translate: TranslateService,
private userPermissionService: UserPermissionService,
private session: SessionService) { }
// Handle the error and map it to the suitable message
@ -46,7 +45,6 @@ export class MessageHandlerService implements ErrorHandler {
this.msgService.announceAppLevelMessage(code, msg, AlertType.DANGER);
// Session is invalid now, clare session cache
this.session.clear();
this.userPermissionService.clearPermissionCache();
} else {
this.msgService.announceMessage(code, msg, AlertType.DANGER);
}

View File

@ -21,7 +21,8 @@ import {
import { SessionService } from '../../shared/session.service';
import { ProjectService } from '@harbor/ui';
import { CommonRoutes } from '@harbor/ui';
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
@Injectable()
export class MemberGuard implements CanActivate, CanActivateChild {
@ -31,49 +32,39 @@ export class MemberGuard implements CanActivate, CanActivateChild {
private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
let projectId = route.params['id'];
const projectId = route.params['id'];
this.sessionService.setProjectMembers([]);
return new Observable((observer) => {
let user = this.sessionService.getCurrentUser();
if (user === null) {
this.sessionService.retrieveUser()
.subscribe(() => {
this.checkMemberStatus(state.url, projectId).subscribe((res) => observer.next(res));
}, error => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
observer.next(false);
});
} else {
this.checkMemberStatus(state.url, projectId).subscribe((res) => observer.next(res));
}
});
}
checkMemberStatus(url: string, projectId: number): Observable<boolean> {
return new Observable<boolean>((observer) => {
this.projectService.checkProjectMember(projectId)
.subscribe(res => {
this.sessionService.setProjectMembers(res);
return observer.next(true);
},
const user = this.sessionService.getCurrentUser();
if (user !== null) {
return this.hasProjectPerm(state.url, projectId);
}
return this.sessionService.retrieveUser().pipe(
() => {
// Add exception for repository in project detail router activation.
this.projectService.getProject(projectId).subscribe(project => {
if (project.metadata && project.metadata.public === 'true') {
return observer.next(true);
}
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return observer.next(false);
},
() => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return observer.next(false);
});
});
});
return this.hasProjectPerm(state.url, projectId);
},
catchError(err => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return of(false);
})
);
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
return this.canActivate(route, state);
}
hasProjectPerm(url: string, projectId: number): Observable<boolean> {
// Note: current user will have the permission to visit the project when the user can get response from GET /projects/:id API.
return this.projectService.getProject(projectId).pipe(
map(() => {
return true;
}),
catchError(err => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return of(false);
})
);
}
}

View File

@ -19,12 +19,12 @@ import {
CanActivateChild
} from '@angular/router';
import { SessionService } from '../../shared/session.service';
import { CommonRoutes, UserPermissionService } from '@harbor/ui';
import { CommonRoutes } from '@harbor/ui';
import { Observable } from 'rxjs';
@Injectable()
export class SignInGuard implements CanActivate, CanActivateChild {
constructor(private authService: SessionService, private router: Router, private userPermission: UserPermissionService) { }
constructor(private authService: SessionService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
// If user has logged in, should not login again
@ -35,7 +35,6 @@ export class SignInGuard implements CanActivate, CanActivateChild {
this.authService.signOff()
.subscribe(() => {
this.authService.clear(); // Destroy session cache
this.userPermission.clearPermissionCache();
return observer.next(true);
}, error => {

View File

@ -21,7 +21,7 @@ import { Member } from '../project/member/member';
import { SignInCredential } from './sign-in-credential';
import { enLang } from '../shared/shared.const';
import { HTTP_FORM_OPTIONS, HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS } from "@harbor/ui";
import { HTTP_FORM_OPTIONS, HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS, FlushAll } from "@harbor/ui";
const signInUrl = '/c/login';
const currentUserEndpoint = "/api/users/current";
@ -62,6 +62,7 @@ export class SessionService {
clear(): void {
this.currentUser = null;
this.projectMembers = [];
FlushAll();
}
// Submit signin form to backend (NOT restful service)

View File

@ -60,14 +60,29 @@ export const enum ConfirmationButtons {
}
export const ProjectTypes = { 0: 'PROJECT.ALL_PROJECTS', 1: 'PROJECT.PRIVATE_PROJECTS', 2: 'PROJECT.PUBLIC_PROJECTS' };
export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST', 4: 'MEMBER.PROJECT_MASTER' };
export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN',
'master': 'MEMBER.PROJECT_MASTER', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' };
export const RoleInfo = {
1: "MEMBER.PROJECT_ADMIN",
2: "MEMBER.DEVELOPER",
3: "MEMBER.GUEST",
4: "MEMBER.PROJECT_MASTER",
5: "MEMBER.LIMITED_GUEST",
};
export const RoleMapping = {
"projectAdmin": "MEMBER.PROJECT_ADMIN",
"master": "MEMBER.PROJECT_MASTER",
"developer": "MEMBER.DEVELOPER",
"guest": "MEMBER.GUEST",
"limitedGuest": "MEMBER.LIMITED_GUEST",
};
export const ProjectRoles = [
{ id: 1, value: "MEMBER.PROJECT_ADMIN" },
{ id: 2, value: "MEMBER.DEVELOPER" },
{ id: 3, value: "MEMBER.GUEST" },
{ id: 4, value: "MEMBER.PROJECT_MASTER" },
{ id: 5, value: "MEMBER.LIMITED_GUEST" },
];
export enum Roles {
@ -75,6 +90,7 @@ export enum Roles {
PROJECT_MASTER = 4,
DEVELOPER = 2,
GUEST = 3,
LIMITED_GUEST = 5,
OTHER = 0,
}
export const DefaultHelmIcon = '/images/helm-gray.svg';

View File

@ -103,7 +103,7 @@ export const maintainUrlQueryParmas = function (uri: string, key: string, value:
str += contentArray[1][getRandomInt(contentArray[1].length)];
}
if (!str.match(/[A-Z]+/g)) {
str += contentArray[1][getRandomInt(contentArray[1].length)];
str += contentArray[2][getRandomInt(contentArray[2].length)];
}
return str;
}

View File

@ -275,6 +275,7 @@
"PROJECT_MASTER": "Master",
"DEVELOPER": "Developer",
"GUEST": "Guest",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Delete",
"ITEMS": "items",
"ACTIONS": "Actions",
@ -621,6 +622,7 @@
"PULL_COMMAND": "Pull Command",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "My Repository",
"PUBLIC_REPOSITORY": "Public Repository",
"DELETION_TITLE_REPO": "Confirm Repository Deletion",
@ -737,7 +739,8 @@
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
@ -1258,10 +1261,10 @@
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
"ADD_SUCCESS": "Added successfully",
"ADD_SUCCESS": "Successfully added ",
"TEST_PASS": "Test passed",
"TEST_FAILED": "Test failed",
"UPDATE_SUCCESS": "Updated successfully",
"UPDATE_SUCCESS": "Successfully updated",
"SCANNER_COLON": "Scanner:",
"NAME_COLON": "Name:",
"VENDOR_COLON": "Vendor:",
@ -1285,6 +1288,10 @@
"ADAPTER": "Adapter",
"VENDOR": "Vendor",
"VERSION": "Version",
"SELECT_SCANNER": "Select Scanner"
"SELECT_SCANNER": "Select Scanner",
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
}
}

View File

@ -276,6 +276,7 @@
"PROJECT_MASTER": "Mantenedor",
"DEVELOPER": "Desarrollador",
"GUEST": "Invitado",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Eliminar",
"ITEMS": "elementos",
"ACTIONS": "Acciones",
@ -622,6 +623,7 @@
"PULL_COMMAND": "Comando Pull",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Mi Repositorio",
"PUBLIC_REPOSITORY": "Repositorio Público",
"DELETION_TITLE_REPO": "Confirmar Eliminación de Repositorio",
@ -738,7 +740,8 @@
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
@ -1255,10 +1258,10 @@
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
"ADD_SUCCESS": "Added successfully",
"ADD_SUCCESS": "Successfully added ",
"TEST_PASS": "Test passed",
"TEST_FAILED": "Test failed",
"UPDATE_SUCCESS": "Updated successfully",
"UPDATE_SUCCESS": "Successfully updated",
"SCANNER_COLON": "Scanner:",
"NAME_COLON": "Name:",
"VENDOR_COLON": "Vendor:",
@ -1282,6 +1285,10 @@
"ADAPTER": "Adapter",
"VENDOR": "Vendor",
"VERSION": "Version",
"SELECT_SCANNER": "Select Scanner"
"SELECT_SCANNER": "Select Scanner",
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
}
}

View File

@ -288,6 +288,7 @@
"PROJECT_MASTER": "préposé à la maintenance",
"DEVELOPER": "Développeur",
"GUEST": "Invité",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Supprimer",
"ITEMS": "items",
"ACTIONS": "Actions",
@ -611,6 +612,7 @@
"PULL_COMMAND": "Commande de Pull",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Mon Dépôt",
"PUBLIC_REPOSITORY": "Dépôt Public",
"DELETION_TITLE_REPO": "Confirmer la Suppresion du Dépôt",
@ -724,7 +726,8 @@
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"
@ -1227,10 +1230,10 @@
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
"ADD_SUCCESS": "Added successfully",
"ADD_SUCCESS": "Successfully added ",
"TEST_PASS": "Test passed",
"TEST_FAILED": "Test failed",
"UPDATE_SUCCESS": "Updated successfully",
"UPDATE_SUCCESS": "Successfully updated",
"SCANNER_COLON": "Scanner:",
"NAME_COLON": "Name:",
"VENDOR_COLON": "Vendor:",
@ -1254,6 +1257,10 @@
"ADAPTER": "Adapter",
"VENDOR": "Vendor",
"VERSION": "Version",
"SELECT_SCANNER": "Select Scanner"
"SELECT_SCANNER": "Select Scanner",
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
}
}

View File

@ -273,6 +273,7 @@
"PROJECT_MASTER": "Mantenedor",
"DEVELOPER": "Desenvolvedor",
"GUEST": "Visitante",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Remover",
"ITEMS": "itens",
"ACTIONS": "Ações",
@ -621,6 +622,7 @@
"PULL_COMMAND": "Comando de Pull",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Meu Repositório",
"PUBLIC_REPOSITORY": "Repositório Público",
"DELETION_TITLE_REPO": "Confirmar remoção de repositório",
@ -733,7 +735,8 @@
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algumas alterações ainda não foram salvas. Você deseja cancelar?"
@ -1252,10 +1255,10 @@
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
"ADD_SUCCESS": "Added successfully",
"ADD_SUCCESS": "Successfully added ",
"TEST_PASS": "Test passed",
"TEST_FAILED": "Test failed",
"UPDATE_SUCCESS": "Updated successfully",
"UPDATE_SUCCESS": "Successfully updated",
"SCANNER_COLON": "Scanner:",
"NAME_COLON": "Name:",
"VENDOR_COLON": "Vendor:",
@ -1279,7 +1282,11 @@
"ADAPTER": "Adapter",
"VENDOR": "Vendor",
"VERSION": "Version",
"SELECT_SCANNER": "Select Scanner"
"SELECT_SCANNER": "Select Scanner",
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
}
}

Some files were not shown because too many files have changed in this diff Show More