Merge branch 'replication_ng' into 190130_transfer_repo

This commit is contained in:
Wenkai Yin 2019-02-27 11:00:42 +08:00 committed by GitHub
commit 95888b3dc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
302 changed files with 8735 additions and 2747 deletions

View File

@ -34,8 +34,9 @@ env:
- CORE_SECRET: tempString
- KEY_PATH: "/data/secretkey"
- REDIS_HOST: localhost
- REG_VERSION: v2.6.2
- REG_VERSION: v2.7.1
- UI_BUILDER_VERSION: 1.6.0
- TOKEN_PRIVATE_KEY_PATH: "/home/travis/gopath/src/github.com/goharbor/harbor/tests/private_key.pem"
addons:
apt:
sources:

View File

@ -95,7 +95,7 @@ VERSIONFILEPATH=$(CURDIR)
VERSIONFILENAME=UIVERSION
#versions
REGISTRYVERSION=v2.6.2
REGISTRYVERSION=v2.7.1
NGINXVERSION=$(VERSIONTAG)
NOTARYVERSION=v0.6.1
CLAIRVERSION=v2.0.7
@ -104,7 +104,7 @@ MIGRATORVERSION=$(VERSIONTAG)
REDISVERSION=$(VERSIONTAG)
# version of chartmuseum
CHARTMUSEUMVERSION=v0.7.1
CHARTMUSEUMVERSION=v0.8.1
# docker parameters
DOCKERCMD=$(shell which docker)

View File

@ -2,13 +2,13 @@
Harbor has integrated with Clair to scan vulnerabilities in images. When Harbor is installed in an environment without internet connection, Clair cannot fetch data from the public vulnerability database. Under this circumstance, Harbor administrator needs to manually update the Clair database.
This document provides step-by-step instructions on updating Clair vulnerability database in Harbor v1.2.
This document provides step-by-step instructions on updating Clair vulnerability database in Harbor.
**NOTE:** Harbor does not ship with any vulnerability data. For this reason, if Harbor cannot connect to Internet, the administrator must manually import vulnerability data to Harbor by using instructions given in this document.
### Preparation
A. You need to install an instance of Clair 2.0.1 with internet connection. If you have another instance of Harbor v1.2 with internet access, it also works.
A. You need to install an instance of Clair with internet connection. If you have another instance of Harbor with internet access, it also works.
B. Check whether your Clair instance has already updated the vulnerability database to the latest version. If it has not, wait for Clair to get the data from public endpoints.
@ -29,28 +29,39 @@ B. Check whether your Clair instance has already updated the vulnerability datab
```
- The phrase "finished fetching" indicates that Clair has finished a round of vulnerability update from an endpoint. Please make sure all five endpoints (rhel, alpine, oracle, debian, ubuntu) are updated correctly.
## Harbor version < 1.6
If you're using a version of Harbor prior to 1.6, you can access the correct instructions for your version using the following URL.
https://github.com/goharbor/harbor/blob/v\<VERSION NUMBER>/docs/import_vulnerability_data.md
## Harbor version >= 1.6
Databased were consolidated in version 1.6 which moved the clair database to the harbor-db container and removed the clair-db container.
### Dumping vulnerability data
- Log in to the host (that is connected to Internet) where Clair database (Postgres) is running.
- Dump Clair's vulnerability database by the following commands, two files (`vulnerability.sql` and `clear.sql`) are generated:
_NOTE: The container name 'clair-db' is a placeholder for the db container used by the internet connected instance of clair_
```
$ docker exec clair-db /bin/bash -c "pg_dump -U postgres -a -t feature -t keyvalue -t namespace -t schema_migrations -t vulnerability -t vulnerability_fixedin_feature" > vulnerability.sql
$ docker exec clair-db /bin/bash -c "pg_dump -U postgres -c -s" > clear.sql
$ docker exec clair-db /bin/sh -c "pg_dump -U postgres -a -t feature -t keyvalue -t namespace -t schema_migrations -t vulnerability -t vulnerability_fixedin_feature" > vulnerability.sql
$ docker exec clair-db /bin/sh -c "pg_dump -U postgres -c -s" > clear.sql
```
### Back up Harbor's Clair database
Before importing the data, it is strongly recommended to back up the Clair database in Harbor.
```
$ docker exec clair-db /bin/bash -c "pg_dump -U postgres -c" > all.sql
$ docker exec harbor-db /bin/sh -c "pg_dump -U postgres -c" > all.sql
```
### Update Harbor's Clair database
Copy the `vulnerability.sql` and `clear.sql` to the host where Harbor is running on. Run the below commands to import the data to Harbor's Clair database:
```
$ docker exec -i clair-db psql -U postgres < clear.sql
$ docker exec -i clair-db psql -U postgres < vulnerability.sql
$ docker exec -i harbor-db psql -U postgres < clear.sql
$ docker exec -i harbor-db psql -U postgres < vulnerability.sql
```
### Rescanning images

45
docs/permissions.md Normal file
View File

@ -0,0 +1,45 @@
# Permissions
Users have different abilities depending on the role they in a project.
On public projects all users will be able to see the list of repositories, images, image vulnerabilities, helm charts and helm chart versions, pull images, retag images (need push permission for destination image), download helm charts, download helm chart versions.
System admin have all permissions for the project.
## Project members permissions
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 | | | | ✓ |

View File

@ -19,6 +19,18 @@ securityDefinitions:
security:
- basicAuth: []
paths:
/health:
get:
summary: 'Health check API'
description: |
The endpoint returns the health stauts of the system.
tags:
- Products
responses:
'200':
description: The system health status.
schema:
$ref: '#/definitions/OverallHealthStatus'
/search:
get:
summary: 'Search for projects, repositories and helm charts'
@ -705,6 +717,37 @@ paths:
$ref: '#/definitions/User'
'401':
description: User need to log in first.
/users/current/permissions:
get:
summary: Get current user permissions.
description: |
This endpoint is to get the current user permissions.
parameters:
- name: scope
in: query
type: string
required: false
description: Get permissions of the scope
- name: relative
in: query
type: boolean
required: false
description: |
If true, the resources in the response are relative to the scope,
eg for resource '/project/1/repository' if relative is 'true' then the resource in response will be 'repository'.
tags:
- Products
responses:
'200':
description: Get current user permission successfully.
schema:
type: array
items:
$ref: '#/definitions/Permission'
'401':
description: User need to log in first.
'500':
description: Internal errors.
'/users/{user_id}':
get:
summary: Get a user's profile.
@ -2706,24 +2749,6 @@ paths:
description: User does not have permission of admin role.
'500':
description: Unexpected internal errors.
/configurations/reset:
post:
summary: Reset system configurations.
description: |
Reset system configurations from environment variables. Can only be accessed by admin user.
tags:
- Products
responses:
'200':
description: Reset system configurations successfully.
'401':
description: User need to log in first.
'403':
description: User does not have permission of admin role.
'415':
$ref: '#/responses/UnsupportedMediaType'
'500':
description: Unexpected internal errors.
/email/ping:
post:
summary: Test connection and authentication with email server.
@ -3136,6 +3161,164 @@ paths:
$ref: '#/definitions/NotFoundChartAPIError'
'500':
$ref: '#/definitions/InternalChartAPIError'
'/projects/{project_id}/robots':
get:
summary: Get all robot accounts of specified project
description: Get all robot accounts of specified project
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
tags:
- Products
- Robot Account
responses:
'200':
description: Get project robot accounts successfully.
schema:
type: array
items:
$ref: '#/definitions/RobotAccount'
'400':
description: The project id is invalid.
'401':
description: User need to log in first.
'403':
description: User in session does not have permission to the project.
'404':
description: Project ID does not exist.
'500':
description: Unexpected internal errors.
post:
summary: Create a robot account for project
description: Create a robot account for project
tags:
- Products
- Robot Account
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: robot
in: body
description: Request body of creating a robot account.
required: true
schema:
$ref: '#/definitions/RobotAccountCreate'
responses:
'201':
description: Project member created successfully.
schema:
$ref: '#/definitions/RobotAccountPostRep'
'400':
description: Project id is not valid.
'401':
description: User need to log in first.
'403':
description: User in session does not have permission to the project.
'409':
description: An robot account with same name already exist in the project.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/robots/{robot_id}':
get:
summary: Return the infor of the specified robot account.
description: Return the infor of the specified robot account.
tags:
- Products
- Robot Account
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: robot_id
in: path
type: integer
format: int64
required: true
description: The ID of robot account.
responses:
'200':
description: Robot account information.
schema:
$ref: '#/definitions/RobotAccount'
'401':
description: User need to log in first.
'403':
description: User in session does not have permission to the project.
'404':
description: The robot account is not found.
'500':
description: Unexpected internal errors.
put:
summary: Update status of robot account.
description: Used to disable/enable a specified robot account.
tags:
- Products
- Robot Account
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: robot_id
in: path
type: integer
format: int64
required: true
description: The ID of robot account.
- name: robot
in: body
description: Request body of enable/disable a robot account.
required: true
schema:
$ref: '#/definitions/RobotAccountUpdate'
responses:
'200':
description: Robot account has been modified success.
'500':
description: Unexpected internal errors.
delete:
summary: Delete the specified robot account
description: Delete the specified robot account
tags:
- Products
- Robot Account
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: robot_id
in: path
type: integer
format: int64
required: true
description: The ID of robot account.
responses:
'200':
description: The specified robot account is successfully deleted.
'401':
description: User need to log in first.
'403':
description: User in session does not have permission to the project.
'404':
description: The robot account is not found.
'500':
description: Unexpected internal errors.
responses:
UnsupportedMediaType:
description: 'The Media Type of the request is not supported, it has to be "application/json"'
@ -4170,7 +4353,7 @@ definitions:
properties:
role_id:
type: integer
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest'
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for master'
member_user:
$ref: '#/definitions/UserEntity'
member_group:
@ -4180,7 +4363,7 @@ definitions:
properties:
role_id:
type: integer
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest'
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for master'
UserEntity:
type: object
properties:
@ -4514,3 +4697,100 @@ definitions:
description: A list of label
items:
$ref: '#/definitions/Label'
OverallHealthStatus:
type: object
description: The system health status
properties:
status:
type: string
description: The overall health status. It is "healthy" only when all the components' status are "healthy"
components:
type: array
items:
$ref: '#/definitions/ComponentHealthStatus'
ComponentHealthStatus:
type: object
description: The health status of component
properties:
name:
type: string
description: The component name
status:
type: string
description: The health status of component
error:
type: string
description: (optional) The error message when the status is "unhealthy"
RobotAccount:
type: object
description: The object of robot account
properties:
id:
type: integer
description: The id of robot account
name:
type: string
description: The name of robot account
description:
type: string
description: The description of robot account
project_id:
type: integer
description: The project id of robot account
disabled:
type: boolean
description: The robot account is disable or enable
creation_time:
type: string
description: The creation time of the robot account
update_time:
type: string
description: The update time of the robot account
RobotAccountCreate:
type: object
properties:
name:
type: string
description: The name of robot account
description:
type: string
description: The description of robot account
access:
type: array
description: The permission of robot account
items:
$ref: '#/definitions/RobotAccountAccess'
RobotAccountPostRep:
type: object
properties:
name:
type: string
description: the name of robot account
token:
type: string
description: the token of robot account
RobotAccountAccess:
type: object
properties:
resource:
type: string
description: the resource of harbor
action:
type: string
description: the action to resource that perdefined in harbor rbac
RobotAccountUpdate:
type: object
properties:
disable:
type: boolean
description: The robot account is disable or enable
Permission:
type: object
description: The permission
properties:
resource:
type: string
description: The permission resoruce
action:
type: string
description: The permission action

View File

@ -94,37 +94,12 @@ services:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "postgresql"
adminserver:
image: goharbor/harbor-adminserver:__version__
container_name: harbor-adminserver
env_file:
- ./common/config/adminserver/env
restart: always
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
volumes:
- /data/config/:/etc/adminserver/config/:z
- /data/secretkey:/etc/adminserver/key:z
- /data/:/data/:z
networks:
- harbor
dns_search: .
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "adminserver"
core:
image: goharbor/harbor-core:__version__
container_name: harbor-core
env_file:
- ./common/config/core/env
- ./common/config/adminserver/env
restart: always
cap_drop:
- ALL
@ -144,7 +119,6 @@ services:
dns_search: .
depends_on:
- log
- adminserver
- registry
logging:
driver: "syslog"
@ -195,7 +169,6 @@ services:
depends_on:
- redis
- core
- adminserver
logging:
driver: "syslog"
options:

View File

@ -1,10 +1,6 @@
CREATE TABLE robot (
id SERIAL PRIMARY KEY NOT NULL,
name varchar(255),
/*
The maximum length of token is 7k
*/
token varchar(7168),
description varchar(1024),
project_id int,
disabled boolean DEFAULT false NOT NULL,

View File

@ -0,0 +1 @@
INSERT INTO role (role_code, name) VALUES ('DRWS', 'master');

View File

@ -148,7 +148,7 @@ _build_chart_server:
@if [ "$(CHARTFLAG)" = "true" ] ; then \
if [ "$(BUILDBIN)" != "true" ] ; then \
rm -rf $(DOCKERFILEPATH_CHART_SERVER)/binary && mkdir -p $(DOCKERFILEPATH_CHART_SERVER)/binary && \
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/chartm, $(DOCKERFILEPATH_CHART_SERVER)/binary/chartm); \
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/chartmuseum/release-$(CHARTMUSEUMVERSION)/chartm, $(DOCKERFILEPATH_CHART_SERVER)/binary/chartm); \
else \
cd $(DOCKERFILEPATH_CHART_SERVER) && $(DOCKERFILEPATH_CHART_SERVER)/builder $(GOBUILDIMAGE) $(CHART_SERVER_CODE_BASE) $(CHARTMUSEUMVERSION) $(CHART_SERVER_MAIN_PATH) $(CHART_SERVER_BIN_NAME) && cd - ; \
fi ; \
@ -184,7 +184,7 @@ _build_notary:
_build_registry:
@if [ "$(BUILDBIN)" != "true" ] ; then \
rm -rf $(DOCKERFILEPATH_REG)/binary && mkdir -p $(DOCKERFILEPATH_REG)/binary && \
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/registry, $(DOCKERFILEPATH_REG)/binary/registry); \
$(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 - ; \
fi
@ -204,9 +204,11 @@ _build_redis:
@echo "Done."
_build_migrator:
@echo "building db migrator container for photon..."
@cd $(DOCKERFILEPATH_MIGRATOR) && $(DOCKERBUILD) -f $(DOCKERFILEPATH_MIGRATOR)/$(DOCKERFILENAME_MIGRATOR) -t $(DOCKERIMAGENAME_MIGRATOR):$(MIGRATORVERSION) .
@echo "Done."
@if [ "$(MIGRATORFLAG)" = "true" ] ; then \
echo "building db migrator container for photon..."; \
cd $(DOCKERFILEPATH_MIGRATOR) && $(DOCKERBUILD) -f $(DOCKERFILEPATH_MIGRATOR)/$(DOCKERFILENAME_MIGRATOR) -t $(DOCKERIMAGENAME_MIGRATOR):$(MIGRATORVERSION) . ; \
echo "Done."; \
fi
define _get_binary
$(WGET) --timeout 30 --no-check-certificate $1 -O $2

View File

@ -8,6 +8,7 @@ RUN tdnf install sudo -y >> /dev/null\
HEALTHCHECK CMD curl --fail -s http://127.0.0.1:8080/api/ping || exit 1
COPY ./make/photon/core/harbor_core ./make/photon/core/start.sh ./UIVERSION /harbor/
COPY ./src/core/views /harbor/views
COPY ./make/migrations /harbor/migrations
RUN chmod u+x /harbor/start.sh /harbor/harbor_core
WORKDIR /harbor/

View File

@ -10,12 +10,15 @@ COPY ./make/photon/log/rsyslog.conf /etc/rsyslog.conf
# rsyslog configuration file for docker
COPY ./make/photon/log/rsyslog_docker.conf /etc/rsyslog.d/
# run logrotate hourly
RUN mv /etc/cron.daily/logrotate /etc/cron.hourly/logrotate
# remove the original "logrotate" in directory "/etc/cron.daily/"
# and copy the customized one to directory "/etc/cron.hourly/"
# to run logrotate hourly
RUN rm /etc/cron.daily/logrotate
COPY ./make/photon/log/logrotate /etc/cron.hourly/
COPY ./make/photon/log/start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/start.sh /etc/rsyslog.d/ && \
chown -R 10000:10000 /etc/rsyslog.conf /etc/rsyslog.d/ /run
chown -R 10000:10000 /etc/rsyslog.conf /etc/rsyslog.d/ /run /var/lib/logrotate/
HEALTHCHECK CMD netstat -ltun|grep 10514

6
make/photon/log/logrotate Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
# run the logrotate with user 10000, the state file "/var/lib/logrotate/logrotate.status"
# is specified to avoid the permission error
sudo -u \#10000 -E /usr/sbin/logrotate -s /var/lib/logrotate/logrotate.status /etc/logrotate.conf
exit 0

View File

@ -1,10 +1,9 @@
FROM golang:1.7.3
FROM golang:1.11
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV DOCKER_BUILDTAGS include_oss include_gcs
WORKDIR $DISTRIBUTION_DIR
COPY . $DISTRIBUTION_DIR
COPY cmd/registry/config-dev.yml /etc/docker/registry/config.yml
RUN make PREFIX=/go clean binaries
RUN CGO_ENABLED=0 make PREFIX=/go clean binaries

View File

@ -22,13 +22,13 @@ cur=$PWD
TEMP=`mktemp -d /$TMPDIR/distribution.XXXXXX`
git clone -b $VERSION https://github.com/docker/distribution.git $TEMP
echo 'build the registry binary bases on the golang:1.11.2...'
echo 'build the registry binary bases on the golang:1.11...'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t registry-golang $TEMP
echo 'copy the registry binary to local...'
ID=$(docker create registry-golang)
docker cp $ID:/go/bin/registry binary
docker cp $ID:/go/src/github.com/docker/distribution/bin binary
docker rm -f $ID
docker rmi -f registry-golang

View File

@ -27,5 +27,52 @@ func TestHandleInternalServerError(t *testing.T) {
if w.Code != http.StatusInternalServerError {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusInternalServerError)
}
}
func TestHandleBadRequestError(t *testing.T) {
w := httptest.NewRecorder()
err := "error message"
handleBadRequestError(w, err)
if w.Code != http.StatusBadRequest {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusBadRequest)
}
}
func TestHandleUnauthorized(t *testing.T) {
w := httptest.NewRecorder()
handleUnauthorized(w)
if w.Code != http.StatusUnauthorized {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusUnauthorized)
}
}
func TestWriteJSONNilInterface(t *testing.T) {
w := httptest.NewRecorder()
if err := writeJSON(w, nil); err != nil {
t.Errorf("Expected nil error, received: %v", err)
}
}
func TestWriteJSONMarshallErr(t *testing.T) {
// Tests capture json.Marshall error
x := map[string]interface{}{
"foo": make(chan int),
}
w := httptest.NewRecorder()
if err := writeJSON(w, x); err == nil {
t.Errorf("Expected %v error received: no no error", err)
}
}
func TestWriteJSON(t *testing.T) {
w := httptest.NewRecorder()
if err := writeJSON(w, "Pong"); err != nil {
t.Errorf("Expected nil error, received: %v", err)
}
}

View File

@ -16,14 +16,16 @@ package config
import (
"fmt"
"os"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/config/metadata"
"github.com/goharbor/harbor/src/common/config/store"
"github.com/goharbor/harbor/src/common/config/store/driver"
"github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"os"
)
// CfgManager ... Configure Manager
@ -48,18 +50,20 @@ func NewRESTCfgManager(configURL, secret string) *CfgManager {
return manager
}
// InmemoryDriver driver for unit testing
type InmemoryDriver struct {
// InMemoryDriver driver for unit testing
type InMemoryDriver struct {
cfgMap map[string]interface{}
}
// Load ...
func (d *InmemoryDriver) Load() (map[string]interface{}, error) {
// Load load data from driver, for example load from database,
// it should be invoked before get any user scope config
// for system scope config, because it is immutable, no need to call this method
func (d *InMemoryDriver) Load() (map[string]interface{}, error) {
return d.cfgMap, nil
}
// Save ...
func (d *InmemoryDriver) Save(cfg map[string]interface{}) error {
// Save only save user config setting to driver, for example: database, REST
func (d *InMemoryDriver) Save(cfg map[string]interface{}) error {
for k, v := range cfg {
d.cfgMap[k] = v
}
@ -68,7 +72,12 @@ func (d *InmemoryDriver) Save(cfg map[string]interface{}) error {
// NewInMemoryManager create a manager for unit testing, doesn't involve database or REST
func NewInMemoryManager() *CfgManager {
return &CfgManager{store: store.NewConfigStore(&InmemoryDriver{cfgMap: map[string]interface{}{}})}
manager := &CfgManager{store: store.NewConfigStore(&InMemoryDriver{cfgMap: map[string]interface{}{}})}
// load default value
manager.loadDefault()
// load system config from env
manager.loadSystemConfigFromEnv()
return manager
}
// loadDefault ...
@ -106,23 +115,47 @@ func (c *CfgManager) loadSystemConfigFromEnv() {
}
}
// GetAll ... Get all settings
func (c *CfgManager) GetAll() []metadata.ConfigureValue {
results := make([]metadata.ConfigureValue, 0)
// GetAll get all settings.
func (c *CfgManager) GetAll() map[string]interface{} {
resultMap := map[string]interface{}{}
if err := c.store.Load(); err != nil {
log.Errorf("GetAll failed, error %v", err)
return results
return resultMap
}
metaDataList := metadata.Instance().GetAll()
for _, item := range metaDataList {
if cfgValue, err := c.store.Get(item.Name); err == nil {
results = append(results, *cfgValue)
cfgValue, err := c.store.GetAnyType(item.Name)
if err != metadata.ErrValueNotSet && err != nil {
log.Errorf("Failed to get value of key %v, error %v", item.Name, err)
continue
}
resultMap[item.Name] = cfgValue
}
return results
return resultMap
}
// Load - Load configuration from storage, like database or redis
// GetUserCfgs retrieve all user configs
func (c *CfgManager) GetUserCfgs() map[string]interface{} {
resultMap := map[string]interface{}{}
if err := c.store.Load(); err != nil {
log.Errorf("GetUserCfgs failed, error %v", err)
return resultMap
}
metaDataList := metadata.Instance().GetAll()
for _, item := range metaDataList {
if item.Scope == metadata.UserScope {
cfgValue, err := c.store.GetAnyType(item.Name)
if err != metadata.ErrValueNotSet && err != nil {
log.Errorf("Failed to get value of key %v, error %v", item.Name, err)
continue
}
resultMap[item.Name] = cfgValue
}
}
return resultMap
}
// Load load configuration from storage, like database or redis
func (c *CfgManager) Load() error {
return c.store.Load()
}
@ -144,7 +177,7 @@ func (c *CfgManager) Get(key string) *metadata.ConfigureValue {
// Set ...
func (c *CfgManager) Set(key string, value interface{}) {
configValue, err := metadata.NewCfgValue(key, fmt.Sprintf("%v", value))
configValue, err := metadata.NewCfgValue(key, utils.GetStrValueOfAnyType(value))
if err != nil {
log.Errorf("error when setting key: %v, error %v", key, err)
return
@ -153,14 +186,6 @@ func (c *CfgManager) Set(key string, value interface{}) {
}
// GetDatabaseCfg - Get database configurations
/*
In database related testing, call it in the TestMain to initialize database schema and set testing configures
cfgMgr := config.NewDBCfgManager()
dao.InitDatabase(cfgMgr.GetDatabaseCfg())
cfgMgr.Load()
cfgMrg.UpdateConfig(testingConfigs)
*/
func (c *CfgManager) GetDatabaseCfg() *models.Database {
return &models.Database{
Type: c.Get(common.DatabaseType).GetString(),
@ -175,7 +200,27 @@ func (c *CfgManager) GetDatabaseCfg() *models.Database {
}
}
// UpdateConfig - Update config store with a specified configuration and also save updated configure
// UpdateConfig - Update config store with a specified configuration and also save updated configure.
func (c *CfgManager) UpdateConfig(cfgs map[string]interface{}) error {
return c.store.Update(cfgs)
}
// ValidateCfg validate config by metadata. return the first error if exist.
func (c *CfgManager) ValidateCfg(cfgs map[string]interface{}) error {
for key, value := range cfgs {
strVal := utils.GetStrValueOfAnyType(value)
_, err := metadata.NewCfgValue(key, strVal)
if err != nil {
return fmt.Errorf("%v, item name: %v", err, key)
}
}
return nil
}
// DumpTrace dump all configurations
func (c *CfgManager) DumpTrace() {
cfgs := c.GetAll()
for k, v := range cfgs {
log.Info(k, ":=", v)
}
}

View File

@ -2,7 +2,7 @@ package config
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/stretchr/testify/assert"
"os"
"testing"
@ -16,32 +16,30 @@ var TestDBConfig = map[string]interface{}{
"postgresql_sslmode": "disable",
"email_host": "127.0.0.1",
"clair_url": "http://clair:6060",
"scan_all_policy": `{"parameter":{"daily_time":0},"type":"daily"}`,
}
var configManager *CfgManager
func TestMain(m *testing.M) {
configManager = NewDBCfgManager()
dao.InitDatabase(configManager.GetDatabaseCfg())
test.InitDatabaseFromEnv()
configManager.UpdateConfig(TestDBConfig)
os.Exit(m.Run())
}
func TestLoadFromDatabase(t *testing.T) {
dao.InitDatabase(configManager.GetDatabaseCfg())
configManager.Load()
configManager.UpdateConfig(TestDBConfig)
configManager.Load()
assert.Equal(t, "127.0.0.1", configManager.Get("email_host").GetString())
assert.Equal(t, "http://clair:6060", configManager.Get("clair_url").GetString())
assert.Equal(t, `{"parameter":{"daily_time":0},"type":"daily"}`, configManager.Get("scan_all_policy").GetString())
}
func TestSaveToDatabase(t *testing.T) {
dao.InitDatabase(configManager.GetDatabaseCfg())
fmt.Printf("database config %#v\n", configManager.GetDatabaseCfg())
configManager.Load()
configManager.Set("read_only", "true")
configManager.UpdateConfig(TestDBConfig)
configManager.Save()
configManager.Load()
assert.Equal(t, true, configManager.Get("read_only").GetBool())
@ -55,7 +53,6 @@ func TestUpdateCfg(t *testing.T) {
"ldap_search_password": "admin",
"ldap_base_dn": "dc=example,dc=com",
}
dao.InitDatabase(configManager.GetDatabaseCfg())
configManager.Load()
configManager.UpdateConfig(testConfig)
@ -111,3 +108,16 @@ func TestNewInMemoryManager(t *testing.T) {
assert.Equal(t, 5, inMemoryManager.Get("ldap_timeout").GetInt())
assert.Equal(t, true, inMemoryManager.Get("ldap_verify_cert").GetBool())
}
/*
func TestNewRESTCfgManager(t *testing.T) {
restMgr := NewRESTCfgManager("http://10.161.47.13:8080"+common.CoreConfigPath, "0XtgSGFx1amMDTaH")
err := restMgr.Load()
if err != nil {
t.Errorf("Failed with error %v", err)
}
fmt.Printf("db:%v", restMgr.GetDatabaseCfg().Type)
fmt.Printf("host:%#v\n", restMgr.GetDatabaseCfg().PostGreSQL.Host)
fmt.Printf("port:%#v\n", restMgr.GetDatabaseCfg().PostGreSQL.Port)
}*/

View File

@ -59,15 +59,15 @@ var (
// 3. CfgManager.Load()/CfgManager.Save() to load/save from configure storage.
ConfigList = []Item{
{Name: "admin_initial_password", Scope: SystemScope, Group: BasicGroup, EnvKey: "HARBOR_ADMIN_PASSWORD", DefaultValue: "", ItemType: &PasswordType{}, Editable: true},
{Name: "admiral_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "ADMIRAL_URL", DefaultValue: "NA", ItemType: &StringType{}, Editable: false},
{Name: "auth_mode", Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &StringType{}, Editable: false},
{Name: "admiral_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "ADMIRAL_URL", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "auth_mode", Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &AuthModeType{}, Editable: false},
{Name: "cfg_expiration", Scope: SystemScope, Group: BasicGroup, EnvKey: "CFG_EXPIRATION", DefaultValue: "5", ItemType: &IntType{}, Editable: false},
{Name: "chart_repository_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "CHART_REPOSITORY_URL", DefaultValue: "http://chartmuseum:9999", ItemType: &StringType{}, Editable: false},
{Name: "clair_db", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB", DefaultValue: "postgres", ItemType: &StringType{}, Editable: false},
{Name: "clair_db_host", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_HOST", DefaultValue: "postgresql", ItemType: &StringType{}, Editable: false},
{Name: "clair_db_password", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_PASSWORD", DefaultValue: "root123", ItemType: &PasswordType{}, Editable: false},
{Name: "clair_db_port", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_PORT", DefaultValue: "5432", ItemType: &IntType{}, Editable: false},
{Name: "clair_db_port", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_PORT", DefaultValue: "5432", ItemType: &PortType{}, Editable: false},
{Name: "clair_db_sslmode", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_SSLMODE", DefaultValue: "disable", ItemType: &StringType{}, Editable: false},
{Name: "clair_db_username", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_USERNAME", DefaultValue: "postgres", ItemType: &StringType{}, Editable: false},
{Name: "clair_url", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_URL", DefaultValue: "http://clair:6060", ItemType: &StringType{}, Editable: false},
@ -80,39 +80,40 @@ var (
{Name: "email_identity", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_IDENTITY", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "email_insecure", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_INSECURE", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: "email_password", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_PWD", DefaultValue: "", ItemType: &PasswordType{}, Editable: false},
{Name: "email_port", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_PORT", DefaultValue: "25", ItemType: &IntType{}, Editable: false},
{Name: "email_port", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_PORT", DefaultValue: "25", ItemType: &PortType{}, Editable: false},
{Name: "email_ssl", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_SSL", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: "email_username", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_USR", DefaultValue: "sample_admin@mydomain.com", ItemType: &StringType{}, Editable: false},
{Name: "ext_endpoint", Scope: SystemScope, Group: BasicGroup, EnvKey: "EXT_ENDPOINT", DefaultValue: "https://host01.com", ItemType: &StringType{}, Editable: false},
{Name: "jobservice_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "JOBSERVICE_URL", DefaultValue: "http://jobservice:8080", ItemType: &StringType{}, Editable: false},
{Name: "ldap_base_dn", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_BASE_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "ldap_base_dn", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_BASE_DN", DefaultValue: "", ItemType: &NonEmptyStringType{}, Editable: false},
{Name: "ldap_filter", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_FILTER", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "ldap_group_base_dn", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_BASE_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "ldap_group_admin_dn", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_ADMIN_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "ldap_group_attribute_name", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_GID", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "ldap_group_search_filter", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_FILTER", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "ldap_group_search_scope", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_SCOPE", DefaultValue: "2", ItemType: &IntType{}, Editable: false},
{Name: "ldap_scope", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SCOPE", DefaultValue: "2", ItemType: &IntType{}, Editable: true},
{Name: "ldap_group_search_scope", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_SCOPE", DefaultValue: "2", ItemType: &LdapScopeType{}, Editable: false},
{Name: "ldap_scope", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SCOPE", DefaultValue: "2", ItemType: &LdapScopeType{}, Editable: false},
{Name: "ldap_search_dn", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SEARCH_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "ldap_search_password", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SEARCH_PWD", DefaultValue: "", ItemType: &PasswordType{}, Editable: false},
{Name: "ldap_timeout", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_TIMEOUT", DefaultValue: "5", ItemType: &IntType{}, Editable: false},
{Name: "ldap_uid", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_UID", DefaultValue: "cn", ItemType: &StringType{}, Editable: true},
{Name: "ldap_url", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_URL", DefaultValue: "", ItemType: &StringType{}, Editable: true},
{Name: "ldap_uid", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_UID", DefaultValue: "cn", ItemType: &NonEmptyStringType{}, Editable: false},
{Name: "ldap_url", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_URL", DefaultValue: "", ItemType: &NonEmptyStringType{}, Editable: false},
{Name: "ldap_verify_cert", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_VERIFY_CERT", DefaultValue: "true", ItemType: &BoolType{}, Editable: false},
{Name: "max_job_workers", Scope: SystemScope, Group: BasicGroup, EnvKey: "MAX_JOB_WORKERS", DefaultValue: "10", ItemType: &IntType{}, Editable: false},
{Name: "notary_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "NOTARY_URL", DefaultValue: "http://notary-server:4443", ItemType: &StringType{}, Editable: false},
{Name: "scan_all_policy", Scope: UserScope, Group: BasicGroup, EnvKey: "", DefaultValue: "", ItemType: &MapType{}, Editable: false},
{Name: "postgresql_database", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_DATABASE", DefaultValue: "registry", ItemType: &StringType{}, Editable: false},
{Name: "postgresql_host", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_HOST", DefaultValue: "postgresql", ItemType: &StringType{}, Editable: false},
{Name: "postgresql_password", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_PASSWORD", DefaultValue: "root123", ItemType: &PasswordType{}, Editable: false},
{Name: "postgresql_port", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_PORT", DefaultValue: "5432", ItemType: &IntType{}, Editable: false},
{Name: "postgresql_port", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_PORT", DefaultValue: "5432", ItemType: &PortType{}, Editable: false},
{Name: "postgresql_sslmode", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_SSLMODE", DefaultValue: "disable", ItemType: &StringType{}, Editable: false},
{Name: "postgresql_username", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_USERNAME", DefaultValue: "postgres", ItemType: &StringType{}, Editable: false},
{Name: "project_creation_restriction", Scope: UserScope, Group: BasicGroup, EnvKey: "PROJECT_CREATION_RESTRICTION", DefaultValue: common.ProCrtRestrEveryone, ItemType: &StringType{}, Editable: false},
{Name: "project_creation_restriction", Scope: UserScope, Group: BasicGroup, EnvKey: "PROJECT_CREATION_RESTRICTION", DefaultValue: common.ProCrtRestrEveryone, ItemType: &ProjectCreationRestrictionType{}, Editable: false},
{Name: "read_only", Scope: UserScope, Group: BasicGroup, EnvKey: "READ_ONLY", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: "registry_storage_provider_name", Scope: SystemScope, Group: BasicGroup, EnvKey: "REGISTRY_STORAGE_PROVIDER_NAME", DefaultValue: "filesystem", ItemType: &StringType{}, Editable: false},
@ -120,7 +121,7 @@ var (
{Name: "registry_controller_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "REGISTRY_CONTROLLER_URL", DefaultValue: "http://registryctl:8080", ItemType: &StringType{}, Editable: false},
{Name: "self_registration", Scope: UserScope, Group: BasicGroup, EnvKey: "SELF_REGISTRATION", DefaultValue: "true", ItemType: &BoolType{}, Editable: false},
{Name: "token_expiration", Scope: UserScope, Group: BasicGroup, EnvKey: "TOKEN_EXPIRATION", DefaultValue: "30", ItemType: &IntType{}, Editable: false},
{Name: "token_service_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "TOKEN_SERVICE_URL", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "token_service_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "TOKEN_SERVICE_URL", DefaultValue: "http://core:8080/service/token", ItemType: &StringType{}, Editable: false},
{Name: "uaa_client_id", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_CLIENTID", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "uaa_client_secret", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_CLIENTSECRET", DefaultValue: "", ItemType: &StringType{}, Editable: false},
@ -128,7 +129,7 @@ var (
{Name: "uaa_verify_cert", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_VERIFY_CERT", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
{Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "true", ItemType: &BoolType{}, Editable: true},
{Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
{Name: "with_notary", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_NOTARY", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
}
)

View File

@ -12,11 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package metadata define config related metadata
package metadata
import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common"
"strconv"
"strings"
)
// Type - Use this interface to define and encapsulate the behavior of validation and transformation
@ -39,6 +43,46 @@ func (t *StringType) get(str string) (interface{}, error) {
return str, nil
}
// NonEmptyStringType ...
type NonEmptyStringType struct {
StringType
}
func (t *NonEmptyStringType) validate(str string) error {
if len(strings.TrimSpace(str)) == 0 {
return ErrStringValueIsEmpty
}
return nil
}
// AuthModeType ...
type AuthModeType struct {
StringType
}
func (t *AuthModeType) validate(str string) error {
if str == common.LDAPAuth || str == common.DBAuth || str == common.UAAAuth {
return nil
}
return fmt.Errorf("invalid %s, shoud be one of %s, %s, %s",
common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth)
}
// ProjectCreationRestrictionType ...
type ProjectCreationRestrictionType struct {
StringType
}
func (t *ProjectCreationRestrictionType) validate(str string) error {
if !(str == common.ProCrtRestrAdmOnly || str == common.ProCrtRestrEveryone) {
return fmt.Errorf("invalid %s, should be %s or %s",
common.ProjectCreationRestriction,
common.ProCrtRestrAdmOnly,
common.ProCrtRestrEveryone)
}
return nil
}
// IntType ..
type IntType struct {
}
@ -48,22 +92,45 @@ func (t *IntType) validate(str string) error {
return err
}
// GetInt ...
func (t *IntType) get(str string) (interface{}, error) {
return strconv.Atoi(str)
}
// PortType ...
type PortType struct {
IntType
}
func (t *PortType) validate(str string) error {
val, err := strconv.Atoi(str)
if err != nil {
return err
}
if val < 0 {
return fmt.Errorf("network port should be greater than 0")
}
if val > 65535 {
return fmt.Errorf("network port should be less than 65535")
}
return err
}
// LdapScopeType - The LDAP scope is a int type, but its is limit to 0, 1, 2
type LdapScopeType struct {
IntType
}
// Validate - Verify the range is limited
// validate - Verify the range is limited
func (t *LdapScopeType) validate(str string) error {
if str == "0" || str == "1" || str == "2" {
return nil
}
return ErrInvalidData
return fmt.Errorf("invalid scope, should be %d, %d or %d",
common.LDAPScopeBase,
common.LDAPScopeOnelevel,
common.LDAPScopeSubtree)
}
// Int64Type ...
@ -75,7 +142,6 @@ func (t *Int64Type) validate(str string) error {
return err
}
// GetInt64 ...
func (t *Int64Type) get(str string) (interface{}, error) {
return strconv.ParseInt(str, 10, 64)
}
@ -116,7 +182,7 @@ func (t *MapType) validate(str string) error {
}
func (t *MapType) get(str string) (interface{}, error) {
result := map[string]string{}
result := map[string]interface{}{}
err := json.Unmarshal([]byte(str), &result)
return result, err
}

View File

@ -94,5 +94,5 @@ func TestMapType_validate(t *testing.T) {
func TestMapType_get(t *testing.T) {
test := &MapType{}
result, _ := test.get(`{"sample":"abc", "another":"welcome"}`)
assert.Equal(t, result, map[string]string{"sample": "abc", "another": "welcome"})
assert.Equal(t, map[string]interface{}{"sample": "abc", "another": "welcome"}, result)
}

View File

@ -29,6 +29,8 @@ var (
ErrInvalidData = errors.New("the data provided is invalid")
// ErrValueNotSet ...
ErrValueNotSet = errors.New("the configure value is not set")
// ErrStringValueIsEmpty ...
ErrStringValueIsEmpty = errors.New("the configure value can not be empty")
)
// ConfigureValue - struct to hold a actual value, also include the name of config metadata.
@ -126,6 +128,14 @@ func (c *ConfigureValue) GetStringToStringMap() map[string]string {
return result
}
// GetAnyType get the interface{} of current value
func (c *ConfigureValue) GetAnyType() (interface{}, error) {
if item, ok := Instance().GetByName(c.Name); ok {
return item.ItemType.get(c.Value)
}
return nil, ErrNotDefined
}
// Validate - to validate configure items, if passed, return nil, else return error
func (c *ConfigureValue) Validate() error {
if item, ok := Instance().GetByName(c.Name); ok {

View File

@ -26,6 +26,7 @@ var testingMetaDataArray = []Item{
{Name: "ulimit", ItemType: &Int64Type{}, Scope: "user", Group: "ldapbasic"},
{Name: "ldap_verify_cert", ItemType: &BoolType{}, Scope: "user", Group: "ldapbasic"},
{Name: "sample_map_setting", ItemType: &MapType{}, Scope: "user", Group: "ldapbasic"},
{Name: "scan_all_policy", ItemType: &MapType{}, Scope: "user", Group: "basic"},
}
// createCfgValue ... Create a ConfigureValue object, only used in test
@ -50,7 +51,11 @@ func TestConfigureValue_GetString(t *testing.T) {
func TestConfigureValue_GetStringToStringMap(t *testing.T) {
Instance().initFromArray(testingMetaDataArray)
assert.Equal(t, createCfgValue("sample_map_setting", `{"sample":"abc"}`).GetStringToStringMap(), map[string]string{"sample": "abc"})
val, err := createCfgValue("sample_map_setting", `{"sample":"abc"}`).GetAnyType()
if err != nil {
t.Error(err)
}
assert.Equal(t, val, map[string]interface{}{"sample": "abc"})
Instance().init()
}
func TestConfigureValue_GetInt(t *testing.T) {
@ -61,3 +66,12 @@ func TestConfigureValue_GetInt64(t *testing.T) {
Instance().initFromArray(testingMetaDataArray)
assert.Equal(t, createCfgValue("ulimit", "99999").GetInt64(), int64(99999))
}
func TestNewScanAllPolicy(t *testing.T) {
Instance().initFromArray(testingMetaDataArray)
value, err := NewCfgValue("scan_all_policy", `{"parameter":{"daily_time":0},"type":"daily"}`)
if err != nil {
t.Errorf("Can not create scan all policy err: %v", err)
}
fmt.Printf("value %v\n", value.GetString())
}

View File

@ -15,12 +15,13 @@
package driver
import (
"fmt"
"github.com/goharbor/harbor/src/common/config/encrypt"
"github.com/goharbor/harbor/src/common/config/metadata"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"os"
)
// Database - Used to load/save configuration in database
@ -61,11 +62,11 @@ func (d *Database) Save(cfgs map[string]interface{}) error {
var configEntries []models.ConfigEntry
for key, value := range cfgs {
if item, ok := metadata.Instance().GetByName(key); ok {
if item.Scope == metadata.SystemScope {
if os.Getenv("UTTEST") != "true" && item.Scope == metadata.SystemScope {
log.Errorf("system setting can not updated, key %v", key)
continue
}
strValue := fmt.Sprintf("%v", value)
strValue := utils.GetStrValueOfAnyType(value)
entry := &models.ConfigEntry{Key: key, Value: strValue}
if _, ok := item.ItemType.(*metadata.PasswordType); ok {
if encryptPassword, err := encrypt.Instance().Encrypt(strValue); err == nil {

View File

@ -14,6 +14,7 @@
package driver
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/stretchr/testify/assert"
"os"
@ -26,18 +27,22 @@ func TestMain(m *testing.M) {
}
func TestDatabase_Load(t *testing.T) {
cfgs := map[string]interface{}{
common.AUTHMode: "db_auth",
common.LDAPURL: "ldap://ldap.vmware.com",
}
driver := Database{}
driver.Save(cfgs)
cfgMap, err := driver.Load()
if err != nil {
t.Errorf("failed to load, error %v", err)
}
assert.True(t, len(cfgMap) > 10)
assert.True(t, len(cfgMap) >= 1)
if _, ok := cfgMap["ldap_url"]; !ok {
t.Error("Can not find ldap_url")
}
}
func TestDatabase_Save(t *testing.T) {

View File

@ -1,29 +1,38 @@
package driver
import (
"errors"
"github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier"
"github.com/goharbor/harbor/src/common/utils/log"
)
// RESTDriver - config store driver based on REST API
type RESTDriver struct {
coreURL string
client *http.Client
configRESTURL string
client *http.Client
}
// NewRESTDriver - Create RESTDriver
func NewRESTDriver(coreURL string, modifiers ...modifier.Modifier) *RESTDriver {
return &RESTDriver{coreURL: coreURL, client: http.NewClient(nil, modifiers...)}
func NewRESTDriver(configRESTURL string, modifiers ...modifier.Modifier) *RESTDriver {
return &RESTDriver{configRESTURL: configRESTURL, client: http.NewClient(nil, modifiers...)}
}
// Load - load config data from REST server
func (h *RESTDriver) Load() (map[string]interface{}, error) {
cfgMap := map[string]interface{}{}
err := h.client.Get(h.coreURL, &cfgMap)
log.Infof("get configuration from url: %+v", h.configRESTURL)
err := h.client.Get(h.configRESTURL, &cfgMap)
if err != nil {
log.Errorf("Failed on load rest config err:%v, url:%v", err, h.configRESTURL)
}
if len(cfgMap) < 1 {
return cfgMap, errors.New("failed to load rest config")
}
return cfgMap, err
}
// Save - Save config data to REST server by PUT method
func (h *RESTDriver) Save(cfgMap map[string]interface{}) error {
return h.client.Put(h.coreURL, cfgMap)
return h.client.Put(h.configRESTURL, cfgMap)
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"github.com/goharbor/harbor/src/common/config/metadata"
"github.com/goharbor/harbor/src/common/config/store/driver"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"sync"
)
@ -30,7 +31,17 @@ func (c *ConfigStore) Get(key string) (*metadata.ConfigureValue, error) {
return nil, errors.New("data in config store is not a ConfigureValue type")
}
return nil, metadata.ErrValueNotSet
}
// GetAnyType get interface{} type for config items
func (c *ConfigStore) GetAnyType(key string) (interface{}, error) {
if value, ok := c.cfgValues.Load(key); ok {
if result, ok := value.(metadata.ConfigureValue); ok {
return result.GetAnyType()
}
return nil, errors.New("data in config store is not a ConfigureValue type")
}
return nil, metadata.ErrValueNotSet
}
// Set - Set configure value in store, not saved to config driver
@ -71,7 +82,6 @@ func (c *ConfigStore) Save() error {
if _, ok := metadata.Instance().GetByName(keyStr); ok {
cfgMap[keyStr] = valueStr
} else {
log.Errorf("failed to get metadata for key %v", keyStr)
}
}
@ -89,7 +99,7 @@ func (c *ConfigStore) Save() error {
func (c *ConfigStore) Update(cfgMap map[string]interface{}) error {
// Update to store
for key, value := range cfgMap {
configValue, err := metadata.NewCfgValue(key, fmt.Sprintf("%v", value))
configValue, err := metadata.NewCfgValue(key, utils.GetStrValueOfAnyType(value))
if err != nil {
log.Warningf("error %v, skip to update configure item, key:%v ", err, key)
delete(cfgMap, key)

View File

@ -19,6 +19,7 @@ const (
DBAuth = "db_auth"
LDAPAuth = "ldap_auth"
UAAAuth = "uaa_auth"
HTTPAuth = "http_auth"
ProCrtRestrEveryone = "everyone"
ProCrtRestrAdmOnly = "adminonly"
LDAPScopeBase = 0
@ -28,6 +29,7 @@ const (
RoleProjectAdmin = 1
RoleDeveloper = 2
RoleGuest = 3
RoleMaster = 4
LabelLevelSystem = "s"
LabelLevelUser = "u"
@ -105,7 +107,6 @@ const (
ClairURL = "clair_url"
NotaryURL = "notary_url"
DefaultAdminserverEndpoint = "http://adminserver:8080"
DefaultJobserviceEndpoint = "http://jobservice:8080"
DefaultCoreEndpoint = "http://core:8080"
DefaultNotaryEndpoint = "http://notary-server:4443"
LdapGroupType = 1
@ -115,46 +116,17 @@ const (
WithChartMuseum = "with_chartmuseum"
ChartRepoURL = "chart_repository_url"
DefaultChartRepoURL = "http://chartmuseum:9999"
DefaultPortalURL = "http://portal"
DefaultRegistryCtlURL = "http://registryctl:8080"
DefaultClairHealthCheckServerURL = "http://clair:6061"
// Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user.
RobotPrefix = "robot$"
CoreConfigPath = "/api/internal/configurations"
)
// TODO remove with adminserver
// Shared variable, not allowed to modify
var (
// the keys of configurations which user can modify in PUT method and user can
// get in GET method
HarborValidKeys = []string{
AUTHMode,
SelfRegistration,
LDAPURL,
LDAPSearchDN,
LDAPSearchPwd,
LDAPBaseDN,
LDAPUID,
LDAPFilter,
LDAPScope,
LDAPTimeout,
LDAPVerifyCert,
LDAPGroupAttributeName,
LDAPGroupBaseDN,
LDAPGroupSearchFilter,
LDAPGroupSearchScope,
LdapGroupAdminDn,
EmailHost,
EmailPort,
EmailUsername,
EmailPassword,
EmailFrom,
EmailSSL,
EmailIdentity,
EmailInsecure,
ProjectCreationRestriction,
TokenExpiration,
ScanAllPolicy,
UAAClientID,
UAAClientSecret,
UAAEndpoint,
UAAVerifyCert,
ReadOnly,
}
// value is default value
HarborStringKeysMap = map[string]string{
@ -195,10 +167,4 @@ var (
UAAVerifyCert: true,
ReadOnly: false,
}
HarborPasswordKeys = []string{
EmailPassword,
LDAPSearchPwd,
UAAClientSecret,
}
)

View File

@ -15,6 +15,7 @@
package dao
import (
"errors"
"fmt"
"strconv"
"strings"
@ -32,6 +33,9 @@ const (
ClairDBAlias = "clair-db"
)
// ErrDupRows is returned by DAO when inserting failed with error "duplicate key value violates unique constraint"
var ErrDupRows = errors.New("sql: duplicate row in DB")
// Database is an interface of different databases
type Database interface {
// Name returns the name of database
@ -86,6 +90,20 @@ func InitDatabase(database *models.Database) error {
return nil
}
// InitAndUpgradeDatabase - init database and upgrade when required
func InitAndUpgradeDatabase(database *models.Database) error {
if err := InitDatabase(database); err != nil {
return err
}
if err := UpgradeSchema(database); err != nil {
return err
}
if err := CheckSchemaVersion(); err != nil {
return err
}
return nil
}
// CheckSchemaVersion checks that whether the schema version matches with the expected one
func CheckSchemaVersion() error {
version, err := GetSchemaVersion()

View File

@ -142,6 +142,7 @@ func TestMain(m *testing.M) {
switch database {
case "postgresql":
PrepareTestForPostgresSQL()
PrepareTestData([]string{"delete from admin_job"}, []string{})
default:
log.Fatalf("invalid database: %s", database)
}

View File

@ -249,7 +249,8 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac
roleID = 2
case common.RoleGuest:
roleID = 3
case common.RoleMaster:
roleID = 4
}
params = append(params, roleID)
}
@ -299,7 +300,7 @@ func GetRolesByLDAPGroup(projectID int64, groupDNCondition string) ([]int, error
}
o := GetOrmer()
// Because an LDAP user can be memberof multiple groups,
// the role is in descent order (1-admin, 2-developer, 3-guest), use min to select the max privilege role.
// the role is in descent order (1-admin, 2-developer, 3-guest, 4-master), use min to select the max privilege role.
sql := fmt.Sprintf(
`select min(pm.role) from project_member pm
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id

View File

@ -17,6 +17,7 @@ package dao
import (
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
"strings"
"time"
)
@ -25,7 +26,14 @@ func AddRobot(robot *models.Robot) (int64, error) {
now := time.Now()
robot.CreationTime = now
robot.UpdateTime = now
return GetOrmer().Insert(robot)
id, err := GetOrmer().Insert(robot)
if err != nil {
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return 0, ErrDupRows
}
return 0, err
}
return id, nil
}
// GetRobotByID ...
@ -79,6 +87,11 @@ func getRobotQuerySetter(query *models.RobotQuery) orm.QuerySeter {
return qs
}
// CountRobot ...
func CountRobot(query *models.RobotQuery) (int64, error) {
return getRobotQuerySetter(query).Count()
}
// UpdateRobot ...
func UpdateRobot(robot *models.Robot) error {
robot.UpdateTime = time.Now()

View File

@ -27,7 +27,6 @@ func TestAddRobot(t *testing.T) {
robotName := "test1"
robot := &models.Robot{
Name: robotName,
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q1",
Description: "test1 description",
ProjectID: 1,
}
@ -46,7 +45,6 @@ func TestGetRobot(t *testing.T) {
robotName := "test2"
robot := &models.Robot{
Name: robotName,
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q2",
Description: "test2 description",
ProjectID: 1,
}
@ -66,7 +64,6 @@ func TestListRobots(t *testing.T) {
robotName := "test3"
robot := &models.Robot{
Name: robotName,
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q3",
Description: "test3 description",
ProjectID: 1,
}
@ -86,7 +83,6 @@ func TestDisableRobot(t *testing.T) {
robotName := "test4"
robot := &models.Robot{
Name: robotName,
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q4",
Description: "test4 description",
ProjectID: 1,
}
@ -111,7 +107,6 @@ func TestEnableRobot(t *testing.T) {
robotName := "test5"
robot := &models.Robot{
Name: robotName,
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q5",
Description: "test5 description",
Disabled: true,
ProjectID: 1,
@ -137,7 +132,6 @@ func TestDeleteRobot(t *testing.T) {
robotName := "test6"
robot := &models.Robot{
Name: robotName,
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q6",
Description: "test6 description",
ProjectID: 1,
}

View File

@ -15,6 +15,8 @@
package models
import (
"github.com/astaxie/beego/validation"
"github.com/goharbor/harbor/src/common/rbac"
"time"
)
@ -25,7 +27,6 @@ const RobotTable = "robot"
type Robot struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
Name string `orm:"column(name)" json:"name"`
Token string `orm:"column(token)" json:"token"`
Description string `orm:"column(description)" json:"description"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
Disabled bool `orm:"column(disabled)" json:"disabled"`
@ -42,7 +43,26 @@ type RobotQuery struct {
Pagination
}
// RobotReq ...
type RobotReq struct {
Name string `json:"name"`
Description string `json:"description"`
Disabled bool `json:"disabled"`
Access []*rbac.Policy `json:"access"`
}
// Valid put request validation
func (rq *RobotReq) Valid(v *validation.Validation) {
// ToDo: add validation for access info.
}
// RobotRep ...
type RobotRep struct {
Name string `json:"name"`
Token string `json:"token"`
}
// TableName ...
func (u *Robot) TableName() string {
func (r *Robot) TableName() string {
return RobotTable
}

View File

@ -20,3 +20,9 @@ type Token struct {
ExpiresIn int `json:"expires_in"`
IssuedAt string `json:"issued_at"`
}
// ResourceActions ...
type ResourceActions struct {
Name string `json:"name"`
Actions []string `json:"actions"`
}

View File

@ -12,15 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package ram
package rbac
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/casbin/casbin"
"github.com/casbin/casbin/model"
"github.com/casbin/casbin/persist"
"github.com/casbin/casbin/util"
)
var (
@ -50,6 +53,30 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == '*')
`
// keyMatch2 determines whether key1 matches the pattern of key2, its behavior most likely the builtin KeyMatch2
// except that the match of ("/project/1/robot", "/project/1") will return false
func keyMatch2(key1 string, key2 string) bool {
key2 = strings.Replace(key2, "/*", "/.*", -1)
re := regexp.MustCompile(`(.*):[^/]+(.*)`)
for {
if !strings.Contains(key2, "/:") {
break
}
key2 = re.ReplaceAllString(key2, "$1[^/]+$2")
}
return util.RegexMatch(key1, "^"+key2+"$")
}
func keyMatch2Func(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
return bool(keyMatch2(name1, name2)), nil
}
type userAdapter struct {
User
}
@ -134,5 +161,8 @@ func (a *userAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex
func enforcerForUser(user User) *casbin.Enforcer {
m := model.Model{}
m.LoadModelFromText(modelText)
return casbin.NewEnforcer(m, &userAdapter{User: user})
e := casbin.NewEnforcer(m, &userAdapter{User: user})
e.AddFunction("keyMatch2", keyMatch2Func)
return e
}

View File

@ -0,0 +1,59 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"testing"
)
func Test_keyMatch2(t *testing.T) {
type args struct {
key1 string
key2 string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "match /project/1/robot, /project/1",
args: args{"/project/1/robot", "/project/1"},
want: false,
},
{
name: "match /project/1/robot, /project/:pid",
args: args{"/project/1/robot", "/project/:pid"},
want: false,
},
{
name: "match /project/1/robot, /project/1/*",
args: args{"/project/1/robot", "/project/1/*"},
want: true,
},
{
name: "match /project/1/robot, /project/:pid/robot",
args: args{"/project/1/robot", "/project/:pid/robot"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := keyMatch2(tt.args.key1, tt.args.key2); got != tt.want {
t.Errorf("keyMatch2() = %v, want %v", got, tt.want)
}
})
}
}

56
src/common/rbac/const.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
// const action variables
const (
ActionAll = Action("*") // action match any other actions
ActionPull = Action("pull") // pull repository tag
ActionPush = Action("push") // push repository tag
ActionPushPull = Action("push+pull") // compatible with security all perm of project
// create, read, update, delete, list actions compatible with restful api methods
ActionCreate = Action("create")
ActionRead = Action("read")
ActionUpdate = Action("update")
ActionDelete = Action("delete")
ActionList = Action("list")
)
// const resource variables
const (
ResourceAll = Resource("*") // resource match any other resources
ResourceConfiguration = Resource("configuration") // project configuration compatible for portal only
ResourceHelmChart = Resource("helm-chart")
ResourceHelmChartVersion = Resource("helm-chart-version")
ResourceHelmChartVersionLabel = Resource("helm-chart-version-label")
ResourceLabel = Resource("label")
ResourceLabelResource = Resource("label-resource")
ResourceLog = Resource("log")
ResourceMember = Resource("member")
ResourceMetadata = Resource("metadata")
ResourceReplication = Resource("replication")
ResourceReplicationJob = Resource("replication-job")
ResourceRepository = Resource("repository")
ResourceRepositoryLabel = Resource("repository-label")
ResourceRepositoryTag = Resource("repository-tag")
ResourceRepositoryTagLabel = Resource("repository-tag-label")
ResourceRepositoryTagManifest = Resource("repository-tag-manifest")
ResourceRepositoryTagScanJob = Resource("repository-tag-scan-job")
ResourceRepositoryTagVulnerability = Resource("repository-tag-vulnerability")
ResourceRobot = Resource("robot")
ResourceSelf = Resource("") // subresource for self
)

View File

@ -0,0 +1,61 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"fmt"
)
// Namespace the namespace interface
type Namespace interface {
// Kind returns the kind of namespace
Kind() string
// Resource returns new resource for subresources with the namespace
Resource(subresources ...Resource) Resource
// Identity returns identity attached with namespace
Identity() interface{}
// IsPublic returns true if namespace is public
IsPublic() bool
}
type projectNamespace struct {
projectIDOrName interface{}
isPublic bool
}
func (ns *projectNamespace) Kind() string {
return "project"
}
func (ns *projectNamespace) Resource(subresources ...Resource) Resource {
return Resource(fmt.Sprintf("/project/%v", ns.projectIDOrName)).Subresource(subresources...)
}
func (ns *projectNamespace) Identity() interface{} {
return ns.projectIDOrName
}
func (ns *projectNamespace) IsPublic() bool {
return ns.isPublic
}
// NewProjectNamespace returns namespace for project
func NewProjectNamespace(projectIDOrName interface{}, isPublic ...bool) Namespace {
isPublicNamespace := false
if len(isPublic) > 0 {
isPublicNamespace = isPublic[0]
}
return &projectNamespace{projectIDOrName: projectIDOrName, isPublic: isPublicNamespace}
}

View File

@ -0,0 +1,45 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ProjectNamespaceTestSuite struct {
suite.Suite
}
func (suite *ProjectNamespaceTestSuite) TestResource() {
var namespace Namespace
namespace = &projectNamespace{projectIDOrName: int64(1)}
suite.Equal(namespace.Resource(Resource("image")), Resource("/project/1/image"))
}
func (suite *ProjectNamespaceTestSuite) TestIdentity() {
namespace, _ := Resource("/project/1/image").GetNamespace()
suite.Equal(namespace.Identity(), int64(1))
namespace, _ = Resource("/project/library/image").GetNamespace()
suite.Equal(namespace.Identity(), "library")
}
func TestProjectNamespaceTestSuite(t *testing.T) {
suite.Run(t, new(ProjectNamespaceTestSuite))
}

50
src/common/rbac/parser.go Normal file
View File

@ -0,0 +1,50 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"errors"
"regexp"
"strconv"
)
var (
namespaceParsers = map[string]namespaceParser{
"project": projectNamespaceParser,
}
)
type namespaceParser func(resource Resource) (Namespace, error)
func projectNamespaceParser(resource Resource) (Namespace, error) {
parserRe := regexp.MustCompile("^/project/([^/]*)/?")
matches := parserRe.FindStringSubmatch(resource.String())
if len(matches) <= 1 {
return nil, errors.New("not support resource")
}
var projectIDOrName interface{}
id, err := strconv.ParseInt(matches[1], 10, 64)
if err == nil {
projectIDOrName = id
} else {
projectIDOrName = matches[1]
}
return &projectNamespace{projectIDOrName: projectIDOrName}, nil
}

View File

@ -0,0 +1,39 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ProjectParserTestSuite struct {
suite.Suite
}
func (suite *ProjectParserTestSuite) TestParse() {
namespace, err := projectNamespaceParser(Resource("/project/1/image"))
suite.Equal(namespace, &projectNamespace{projectIDOrName: int64(1)})
suite.Nil(err)
namespace, err = projectNamespaceParser(Resource("/fake/1/image"))
suite.Nil(namespace)
suite.Error(err)
}
func TestProjectParserTestSuite(t *testing.T) {
suite.Run(t, new(ProjectParserTestSuite))
}

View File

@ -0,0 +1,166 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"github.com/goharbor/harbor/src/common/rbac"
)
var (
// subresource policies for public project
publicProjectPolicies = []*rbac.Policy{
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagLabel, 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},
}
// 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.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.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.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project
{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},
}
)
// PoliciesForPublicProject ...
func PoliciesForPublicProject(namespace rbac.Namespace) []*rbac.Policy {
policies := []*rbac.Policy{}
for _, policy := range publicProjectPolicies {
policies = append(policies, &rbac.Policy{
Resource: namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}
// GetAllPolicies returns all policies for namespace of the project
func GetAllPolicies(namespace rbac.Namespace) []*rbac.Policy {
policies := []*rbac.Policy{}
for _, policy := range allPolicies {
policies = append(policies, &rbac.Policy{
Resource: namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}

View File

@ -0,0 +1,83 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"github.com/goharbor/harbor/src/common/rbac"
)
// visitorContext the context interface for the project visitor
type visitorContext interface {
IsAuthenticated() bool
// GetUsername returns the username of user related to the context
GetUsername() string
// IsSysAdmin returns whether the user is system admin
IsSysAdmin() bool
}
// visitor implement the rbac.User interface for project visitor
type visitor struct {
ctx visitorContext
namespace rbac.Namespace
projectRoles []int
}
// GetUserName returns username of the visitor
func (v *visitor) GetUserName() string {
// anonymous username for unauthenticated Visitor
if !v.ctx.IsAuthenticated() {
return "anonymous"
}
return v.ctx.GetUsername()
}
// GetPolicies returns policies of the visitor
func (v *visitor) GetPolicies() []*rbac.Policy {
if v.ctx.IsSysAdmin() {
return GetAllPolicies(v.namespace)
}
if v.namespace.IsPublic() {
return PoliciesForPublicProject(v.namespace)
}
return nil
}
// GetRoles returns roles of the visitor
func (v *visitor) GetRoles() []rbac.Role {
// Ignore roles when visitor is anonymous or system admin
if !v.ctx.IsAuthenticated() || v.ctx.IsSysAdmin() {
return nil
}
roles := []rbac.Role{}
for _, roleID := range v.projectRoles {
roles = append(roles, &visitorRole{roleID: roleID, namespace: v.namespace})
}
return roles
}
// NewUser returns rbac.User interface for the project visitor
func NewUser(ctx visitorContext, namespace rbac.Namespace, projectRoles ...int) rbac.User {
return &visitor{
ctx: ctx,
namespace: namespace,
projectRoles: projectRoles,
}
}

View File

@ -0,0 +1,301 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
)
var (
rolePoliciesMap = map[string][]*rbac.Policy{
"projectAdmin": {
{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.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.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationJob, 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.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project
{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}, // upload helm chart
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, // download helm chart
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, // upload helm chart version
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, // read and download helm chart version
{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},
},
"master": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{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.ActionRead},
{Resource: rbac.ResourceReplication, 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.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.ActionPush},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{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.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
},
"developer": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, 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.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{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.ActionList},
{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.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{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.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
},
"guest": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagLabel, 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},
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
},
}
)
// visitorRole implement the rbac.Role interface
type visitorRole struct {
namespace rbac.Namespace
roleID int
}
// GetRoleName returns role name for the visitor role
func (role *visitorRole) GetRoleName() string {
switch role.roleID {
case common.RoleProjectAdmin:
return "projectAdmin"
case common.RoleMaster:
return "master"
case common.RoleDeveloper:
return "developer"
case common.RoleGuest:
return "guest"
default:
return ""
}
}
// GetPolicies returns policies for the visitor role
func (role *visitorRole) GetPolicies() []*rbac.Policy {
policies := []*rbac.Policy{}
roleName := role.GetRoleName()
if roleName == "" {
return policies
}
for _, policy := range rolePoliciesMap[roleName] {
policies = append(policies, &rbac.Policy{
Resource: role.namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}

View File

@ -0,0 +1,44 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/stretchr/testify/suite"
)
type VisitorRoleTestSuite struct {
suite.Suite
}
func (suite *VisitorRoleTestSuite) TestGetRoleName() {
projectAdmin := visitorRole{roleID: common.RoleProjectAdmin}
suite.Equal(projectAdmin.GetRoleName(), "projectAdmin")
developer := visitorRole{roleID: common.RoleDeveloper}
suite.Equal(developer.GetRoleName(), "developer")
guest := visitorRole{roleID: common.RoleGuest}
suite.Equal(guest.GetRoleName(), "guest")
unknow := visitorRole{roleID: 404}
suite.Equal(unknow.GetRoleName(), "")
}
func TestVisitorRoleTestSuite(t *testing.T) {
suite.Run(t, new(VisitorRoleTestSuite))
}

View File

@ -0,0 +1,93 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/stretchr/testify/suite"
)
type fakeVisitorContext struct {
username string
isSysAdmin bool
}
func (ctx *fakeVisitorContext) IsAuthenticated() bool {
return ctx.username != ""
}
func (ctx *fakeVisitorContext) GetUsername() string {
return ctx.username
}
func (ctx *fakeVisitorContext) IsSysAdmin() bool {
return ctx.IsAuthenticated() && ctx.isSysAdmin
}
var (
anonymousCtx = &fakeVisitorContext{}
authenticatedCtx = &fakeVisitorContext{username: "user"}
sysAdminCtx = &fakeVisitorContext{username: "admin", isSysAdmin: true}
)
type VisitorTestSuite struct {
suite.Suite
}
func (suite *VisitorTestSuite) TestGetPolicies() {
namespace := rbac.NewProjectNamespace("library", false)
publicNamespace := rbac.NewProjectNamespace("library", true)
anonymous := NewUser(anonymousCtx, namespace)
suite.Nil(anonymous.GetPolicies())
anonymousForPublicProject := NewUser(anonymousCtx, publicNamespace)
suite.Equal(anonymousForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
authenticated := NewUser(authenticatedCtx, namespace)
suite.Nil(authenticated.GetPolicies())
authenticatedForPublicProject := NewUser(authenticatedCtx, publicNamespace)
suite.Equal(authenticatedForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
systemAdmin := NewUser(sysAdminCtx, namespace)
suite.Equal(systemAdmin.GetPolicies(), GetAllPolicies(namespace))
systemAdminForPublicProject := NewUser(sysAdminCtx, publicNamespace)
suite.Equal(systemAdminForPublicProject.GetPolicies(), GetAllPolicies(publicNamespace))
}
func (suite *VisitorTestSuite) TestGetRoles() {
namespace := rbac.NewProjectNamespace("library", false)
anonymous := NewUser(anonymousCtx, namespace)
suite.Nil(anonymous.GetRoles())
authenticated := NewUser(authenticatedCtx, namespace)
suite.Empty(authenticated.GetRoles())
authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin)
suite.Len(authenticated.GetRoles(), 1)
authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin, common.RoleDeveloper)
suite.Len(authenticated.GetRoles(), 2)
}
func TestVisitorTestSuite(t *testing.T) {
suite.Run(t, new(VisitorTestSuite))
}

View File

@ -12,10 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package ram
package rbac
import (
"errors"
"fmt"
"path"
"strings"
)
const (
@ -28,6 +31,27 @@ const (
// Resource the type of resource
type Resource string
// RelativeTo returns relative resource to other resource
func (res Resource) RelativeTo(other Resource) (Resource, error) {
prefix := other.String()
str := res.String()
if !strings.HasPrefix(str, prefix) {
return Resource(""), errors.New("value error")
}
relative := strings.TrimPrefix(str, prefix)
if strings.HasPrefix(relative, "/") {
relative = relative[1:]
}
if relative == "" {
relative = "."
}
return Resource(relative), nil
}
func (res Resource) String() string {
return string(res)
}
@ -43,6 +67,18 @@ func (res Resource) Subresource(resources ...Resource) Resource {
return Resource(path.Join(elements...))
}
// GetNamespace returns namespace from resource
func (res Resource) GetNamespace() (Namespace, error) {
for _, parser := range namespaceParsers {
namespace, err := parser(res)
if err == nil {
return namespace, nil
}
}
return nil, fmt.Errorf("no namespace found for %s", res)
}
// Action the type of action
type Action string
@ -74,14 +110,14 @@ func (p *Policy) GetEffect() string {
return eft.String()
}
// Role the interface of ram role
// Role the interface of rbac role
type Role interface {
// GetRoleName returns the role identity, if empty string role's policies will be ignore
GetRoleName() string
GetPolicies() []*Policy
}
// User the interface of ram user
// User the interface of rbac user
type User interface {
// GetUserName returns the user identity, if empty string user's all policies will be ignore
GetUserName() string

View File

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package ram
package rbac
import (
"reflect"
"testing"
)
@ -126,7 +127,7 @@ func TestHasPermissionUserWithoutRoles(t *testing.T) {
{
name: "project create for user without roles",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}},
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
"/project",
"create",
},
@ -135,7 +136,7 @@ func TestHasPermissionUserWithoutRoles(t *testing.T) {
{
name: "project delete test for user without roles",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}},
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
"/project",
"delete",
},
@ -167,7 +168,7 @@ func TestHasPermissionUsernameEmpty(t *testing.T) {
{
name: "project create for user without roles",
args: args{
&userWithoutRoles{Username: "", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}},
&userWithoutRoles{Username: "", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
"/project",
"create",
},
@ -355,3 +356,84 @@ func TestResource_Subresource(t *testing.T) {
})
}
}
func TestResource_GetNamespace(t *testing.T) {
tests := []struct {
name string
res Resource
want Namespace
wantErr bool
}{
{
name: "project namespace",
res: Resource("/project/1"),
want: &projectNamespace{int64(1), false},
wantErr: false,
},
{
name: "unknow namespace",
res: Resource("/unknow/1"),
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.res.GetNamespace()
if (err != nil) != tt.wantErr {
t.Errorf("Resource.GetNamespace() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Resource.GetNamespace() = %v, want %v", got, tt.want)
}
})
}
}
func TestResource_RelativeTo(t *testing.T) {
type args struct {
other Resource
}
tests := []struct {
name string
res Resource
args args
want Resource
wantErr bool
}{
{
name: "/project/1/image",
res: Resource("/project/1/image"),
args: args{other: Resource("/project/1")},
want: Resource("image"),
wantErr: false,
},
{
name: "/project/1",
res: Resource("/project/1"),
args: args{other: Resource("/project/1")},
want: Resource("."),
wantErr: false,
},
{
name: "/project/1",
res: Resource("/project/1"),
args: args{other: Resource("/system")},
want: Resource(""),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.res.RelativeTo(tt.args.other)
if (err != nil) != tt.wantErr {
t.Errorf("Resource.RelativeTo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Resource.RelativeTo() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -15,10 +15,10 @@
package admiral
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/security/admiral/authcontext"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr"
)
@ -69,72 +69,17 @@ func (s *SecurityContext) IsSolutionUser() bool {
return false
}
// HasReadPerm returns whether the user has read permission to the project
func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
public, err := s.pm.IsPublic(projectIDOrName)
if err != nil {
log.Errorf("failed to check the public of project %v: %v",
projectIDOrName, err)
return false
}
if public {
return true
}
// private project
if !s.IsAuthenticated() {
return false
}
// system admin
if s.IsSysAdmin() {
return true
}
roles := s.GetProjectRoles(projectIDOrName)
return len(roles) > 0
}
// HasWritePerm returns whether the user has write permission to the project
func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
if !s.IsAuthenticated() {
return false
}
// system admin
if s.IsSysAdmin() {
return true
}
roles := s.GetProjectRoles(projectIDOrName)
for _, role := range roles {
switch role {
case common.RoleProjectAdmin,
common.RoleDeveloper:
return true
}
}
return false
}
// HasAllPerm returns whether the user has all permissions to the project
func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
if !s.IsAuthenticated() {
return false
}
// system admin
if s.IsSysAdmin() {
return true
}
roles := s.GetProjectRoles(projectIDOrName)
for _, role := range roles {
switch role {
case common.RoleProjectAdmin:
return true
// Can returns whether the user can do action on resource
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
ns, err := resource.GetNamespace()
if err == nil {
switch ns.Kind() {
case "project":
projectIDOrName := ns.Identity()
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject)
user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...)
return rbac.HasPermission(user, resource, action)
}
}

View File

@ -16,6 +16,7 @@ package security
import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
)
// Context abstracts the operations related with authN and authZ
@ -28,14 +29,10 @@ type Context interface {
IsSysAdmin() bool
// IsSolutionUser returns whether the user is solution user
IsSolutionUser() bool
// HasReadPerm returns whether the user has read permission to the project
HasReadPerm(projectIDOrName interface{}) bool
// HasWritePerm returns whether the user has write permission to the project
HasWritePerm(projectIDOrName interface{}) bool
// HasAllPerm returns whether the user has all permissions to the project
HasAllPerm(projectIDOrName interface{}) bool
// Get current user's all project
GetMyProjects() ([]*models.Project, error)
// Get user's role in provided project
GetProjectRoles(projectIDOrName interface{}) []int
// Can returns whether the user can do action on resource
Can(action rbac.Action, resource rbac.Resource) bool
}

View File

@ -19,6 +19,8 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/group"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr"
)
@ -65,69 +67,20 @@ func (s *SecurityContext) IsSolutionUser() bool {
return false
}
// HasReadPerm returns whether the user has read permission to the project
func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
// public project
public, err := s.pm.IsPublic(projectIDOrName)
if err != nil {
log.Errorf("failed to check the public of project %v: %v",
projectIDOrName, err)
return false
}
if public {
return true
}
// private project
if !s.IsAuthenticated() {
return false
}
// system admin
if s.IsSysAdmin() {
return true
}
roles := s.GetProjectRoles(projectIDOrName)
return len(roles) > 0
}
// HasWritePerm returns whether the user has write permission to the project
func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
if !s.IsAuthenticated() {
return false
}
// system admin
if s.IsSysAdmin() {
return true
}
roles := s.GetProjectRoles(projectIDOrName)
for _, role := range roles {
switch role {
case common.RoleProjectAdmin,
common.RoleDeveloper:
return true
// Can returns whether the user can do action on resource
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
ns, err := resource.GetNamespace()
if err == nil {
switch ns.Kind() {
case "project":
projectIDOrName := ns.Identity()
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject)
user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...)
return rbac.HasPermission(user, resource, action)
}
}
return false
}
// HasAllPerm returns whether the user has all permissions to the project
func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
if !s.IsAuthenticated() {
return false
}
// system admin
if s.IsSysAdmin() {
return true
}
roles := s.GetProjectRoles(projectIDOrName)
for _, role := range roles {
switch role {
case common.RoleProjectAdmin:
return true
}
}
return false
}
@ -167,6 +120,8 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
switch role.RoleCode {
case "MDRWS":
roles = append(roles, common.RoleProjectAdmin)
case "DRWS":
roles = append(roles, common.RoleMaster)
case "RWS":
roles = append(roles, common.RoleDeveloper)
case "RS":

View File

@ -16,14 +16,15 @@ package local
import (
"os"
"strconv"
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/project"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
"github.com/stretchr/testify/assert"
@ -53,45 +54,8 @@ var (
)
func TestMain(m *testing.M) {
dbHost := os.Getenv("POSTGRESQL_HOST")
if len(dbHost) == 0 {
log.Fatalf("environment variable POSTGRES_HOST is not set")
}
dbUser := os.Getenv("POSTGRESQL_USR")
if len(dbUser) == 0 {
log.Fatalf("environment variable POSTGRES_USR is not set")
}
dbPortStr := os.Getenv("POSTGRESQL_PORT")
if len(dbPortStr) == 0 {
log.Fatalf("environment variable POSTGRES_PORT is not set")
}
dbPort, err := strconv.Atoi(dbPortStr)
if err != nil {
log.Fatalf("invalid POSTGRESQL_PORT: %v", err)
}
dbPassword := os.Getenv("POSTGRESQL_PWD")
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
}
database := &models.Database{
Type: "postgresql",
PostGreSQL: &models.PostGreSQL{
Host: dbHost,
Port: dbPort,
Username: dbUser,
Password: dbPassword,
Database: dbDatabase,
},
}
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
test.InitDatabaseFromEnv()
// regiser users
id, err := dao.Register(*projectAdminUser)
@ -210,66 +174,73 @@ func TestIsSolutionUser(t *testing.T) {
func TestHasReadPerm(t *testing.T) {
// public project
ctx := NewSecurityContext(nil, pm)
assert.True(t, ctx.HasReadPerm("library"))
resource := rbac.NewProjectNamespace("library").Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(rbac.ActionPull, resource))
// private project, unauthenticated
ctx = NewSecurityContext(nil, pm)
assert.False(t, ctx.HasReadPerm(private.Name))
resource = rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
assert.False(t, ctx.Can(rbac.ActionPull, resource))
// private project, authenticated, has no perm
ctx = NewSecurityContext(&models.User{
Username: "test",
}, pm)
assert.False(t, ctx.HasReadPerm(private.Name))
assert.False(t, ctx.Can(rbac.ActionPull, resource))
// private project, authenticated, has read perm
ctx = NewSecurityContext(guestUser, pm)
assert.True(t, ctx.HasReadPerm(private.Name))
assert.True(t, ctx.Can(rbac.ActionPull, resource))
// private project, authenticated, system admin
ctx = NewSecurityContext(&models.User{
Username: "admin",
HasAdminRole: true,
}, pm)
assert.True(t, ctx.HasReadPerm(private.Name))
assert.True(t, ctx.Can(rbac.ActionPull, resource))
}
func TestHasWritePerm(t *testing.T) {
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
// unauthenticated
ctx := NewSecurityContext(nil, pm)
assert.False(t, ctx.HasWritePerm(private.Name))
assert.False(t, ctx.Can(rbac.ActionPush, resource))
// authenticated, has read perm
ctx = NewSecurityContext(guestUser, pm)
assert.False(t, ctx.HasWritePerm(private.Name))
assert.False(t, ctx.Can(rbac.ActionPush, resource))
// authenticated, has write perm
ctx = NewSecurityContext(developerUser, pm)
assert.True(t, ctx.HasWritePerm(private.Name))
assert.True(t, ctx.Can(rbac.ActionPush, resource))
// authenticated, system admin
ctx = NewSecurityContext(&models.User{
Username: "admin",
HasAdminRole: true,
}, pm)
assert.True(t, ctx.HasReadPerm(private.Name))
assert.True(t, ctx.Can(rbac.ActionPush, resource))
}
func TestHasAllPerm(t *testing.T) {
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
// unauthenticated
ctx := NewSecurityContext(nil, pm)
assert.False(t, ctx.HasAllPerm(private.Name))
assert.False(t, ctx.Can(rbac.ActionPushPull, resource))
// authenticated, has all perms
ctx = NewSecurityContext(projectAdminUser, pm)
assert.True(t, ctx.HasAllPerm(private.Name))
assert.True(t, ctx.Can(rbac.ActionPushPull, resource))
// authenticated, system admin
ctx = NewSecurityContext(&models.User{
Username: "admin",
HasAdminRole: true,
}, pm)
assert.True(t, ctx.HasAllPerm(private.Name))
assert.True(t, ctx.Can(rbac.ActionPushPull, resource))
}
func TestHasAllPermWithGroup(t *testing.T) {
@ -285,10 +256,13 @@ func TestHasAllPermWithGroup(t *testing.T) {
developer.GroupList = []*models.UserGroup{
{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"},
}
resource := rbac.NewProjectNamespace(project.Name).Resource(rbac.ResourceRepository)
ctx := NewSecurityContext(developer, pm)
assert.False(t, ctx.HasAllPerm(project.Name))
assert.True(t, ctx.HasWritePerm(project.Name))
assert.True(t, ctx.HasReadPerm(project.Name))
assert.False(t, ctx.Can(rbac.ActionPushPull, resource))
assert.True(t, ctx.Can(rbac.ActionPush, resource))
assert.True(t, ctx.Can(rbac.ActionPull, resource))
}
func TestGetMyProjects(t *testing.T) {

View File

@ -0,0 +1,88 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/promgr"
)
// SecurityContext implements security.Context interface based on database
type SecurityContext struct {
robot *models.Robot
pm promgr.ProjectManager
policy []*rbac.Policy
}
// NewSecurityContext ...
func NewSecurityContext(robot *models.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext {
return &SecurityContext{
robot: robot,
pm: pm,
policy: policy,
}
}
// IsAuthenticated returns true if the user has been authenticated
func (s *SecurityContext) IsAuthenticated() bool {
return s.robot != nil
}
// GetUsername returns the username of the authenticated user
// It returns null if the user has not been authenticated
func (s *SecurityContext) GetUsername() string {
if !s.IsAuthenticated() {
return ""
}
return s.robot.Name
}
// IsSysAdmin robot cannot be a system admin
func (s *SecurityContext) IsSysAdmin() bool {
return false
}
// IsSolutionUser robot cannot be a system admin
func (s *SecurityContext) IsSolutionUser() bool {
return false
}
// GetMyProjects no implementation
func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
return nil, nil
}
// GetProjectRoles no implementation
func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
return nil
}
// Can returns whether the robot can do action on resource
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
ns, err := resource.GetNamespace()
if err == nil {
switch ns.Kind() {
case "project":
projectIDOrName := ns.Identity()
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject)
robot := NewRobot(s.GetUsername(), projectNamespace, s.policy)
return rbac.HasPermission(robot, resource, action)
}
}
return false
}

View File

@ -0,0 +1,200 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"os"
"strconv"
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
private = &models.Project{
Name: "testrobot",
OwnerID: 1,
}
pm = promgr.NewDefaultProjectManager(local.NewDriver(), true)
)
func TestMain(m *testing.M) {
dbHost := os.Getenv("POSTGRESQL_HOST")
if len(dbHost) == 0 {
log.Fatalf("environment variable POSTGRES_HOST is not set")
}
dbUser := os.Getenv("POSTGRESQL_USR")
if len(dbUser) == 0 {
log.Fatalf("environment variable POSTGRES_USR is not set")
}
dbPortStr := os.Getenv("POSTGRESQL_PORT")
if len(dbPortStr) == 0 {
log.Fatalf("environment variable POSTGRES_PORT is not set")
}
dbPort, err := strconv.Atoi(dbPortStr)
if err != nil {
log.Fatalf("invalid POSTGRESQL_PORT: %v", err)
}
dbPassword := os.Getenv("POSTGRESQL_PWD")
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
}
database := &models.Database{
Type: "postgresql",
PostGreSQL: &models.PostGreSQL{
Host: dbHost,
Port: dbPort,
Username: dbUser,
Password: dbPassword,
Database: dbDatabase,
},
}
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
// add project
id, err := dao.AddProject(*private)
if err != nil {
log.Fatalf("failed to add project: %v", err)
}
private.ProjectID = id
defer dao.DeleteProject(id)
os.Exit(m.Run())
}
func TestIsAuthenticated(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil, nil)
assert.False(t, ctx.IsAuthenticated())
// authenticated
ctx = NewSecurityContext(&models.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
assert.True(t, ctx.IsAuthenticated())
}
func TestGetUsername(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil, nil)
assert.Equal(t, "", ctx.GetUsername())
// authenticated
ctx = NewSecurityContext(&models.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
assert.Equal(t, "test", ctx.GetUsername())
}
func TestIsSysAdmin(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil, nil)
assert.False(t, ctx.IsSysAdmin())
// authenticated, non admin
ctx = NewSecurityContext(&models.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
assert.False(t, ctx.IsSysAdmin())
}
func TestIsSolutionUser(t *testing.T) {
ctx := NewSecurityContext(nil, nil, nil)
assert.False(t, ctx.IsSolutionUser())
}
func TestHasReadPerm(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/testrobot/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := &models.Robot{
Name: "test_robot_1",
Description: "desc",
}
ctx := NewSecurityContext(robot, pm, policies)
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(rbac.ActionPull, resource))
}
func TestHasWritePerm(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/testrobot/repository",
Action: "push",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := &models.Robot{
Name: "test_robot_2",
Description: "desc",
}
ctx := NewSecurityContext(robot, pm, policies)
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(rbac.ActionPush, resource))
}
func TestHasAllPerm(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/testrobot/repository",
Action: "push+pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := &models.Robot{
Name: "test_robot_3",
Description: "desc",
}
ctx := NewSecurityContext(robot, pm, policies)
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(rbac.ActionPushPull, resource))
}
func TestGetMyProjects(t *testing.T) {
ctx := NewSecurityContext(nil, nil, nil)
projects, err := ctx.GetMyProjects()
require.Nil(t, err)
assert.Nil(t, projects)
}
func TestGetProjectRoles(t *testing.T) {
ctx := NewSecurityContext(nil, nil, nil)
roles := ctx.GetProjectRoles("test")
assert.Nil(t, roles)
}

View File

@ -0,0 +1,42 @@
package robot
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
)
// robot implement the rbac.User interface for project robot account
type robot struct {
username string
namespace rbac.Namespace
policy []*rbac.Policy
}
// GetUserName get the robot name.
func (r *robot) GetUserName() string {
return r.username
}
// GetPolicies ...
func (r *robot) GetPolicies() []*rbac.Policy {
policies := []*rbac.Policy{}
if r.namespace.IsPublic() {
policies = append(policies, project.PoliciesForPublicProject(r.namespace)...)
}
policies = append(policies, r.policy...)
return policies
}
// GetRoles robot has no definition of role, always return nil here.
func (r *robot) GetRoles() []rbac.Role {
return nil
}
// NewRobot ...
func NewRobot(username string, namespace rbac.Namespace, policy []*rbac.Policy) rbac.User {
return &robot{
username: username,
namespace: namespace,
policy: policy,
}
}

View File

@ -0,0 +1,27 @@
package robot
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/stretchr/testify/assert"
"testing"
)
func TestGetPolicies(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := robot{
username: "test",
namespace: rbac.NewProjectNamespace("library", false),
policy: policies,
}
assert.Equal(t, robot.GetUserName(), "test")
assert.NotNil(t, robot.GetPolicies())
assert.Nil(t, robot.GetRoles())
}

View File

@ -19,6 +19,7 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/secret"
"github.com/goharbor/harbor/src/common/utils/log"
)
@ -70,27 +71,10 @@ func (s *SecurityContext) IsSolutionUser() bool {
return s.IsAuthenticated()
}
// HasReadPerm returns true if the corresponding user of the secret
// Can returns whether the user can do action on resource
// returns true if the corresponding user of the secret
// is jobservice or core service, otherwise returns false
func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
if s.store == nil {
return false
}
return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser
}
// HasWritePerm returns true if the corresponding user of the secret
// is jobservice or core service, otherwise returns false
func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
if s.store == nil {
return false
}
return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser
}
// HasAllPerm returns true if the corresponding user of the secret
// is jobservice or core service, otherwise returns false
func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
if s.store == nil {
return false
}
@ -105,7 +89,9 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
// GetProjectRoles return guest role if has read permission, otherwise return nil
func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
roles := []int{}
if s.HasReadPerm(projectIDOrName) {
if s.store != nil &&
(s.store.GetUsername(s.secret) == secret.JobserviceUser ||
s.store.GetUsername(s.secret) == secret.CoreUser) {
roles = append(roles, common.RoleGuest)
}
return roles

View File

@ -18,6 +18,7 @@ import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/secret"
"github.com/stretchr/testify/assert"
)
@ -96,9 +97,11 @@ func TestIsSolutionUser(t *testing.T) {
}
func TestHasReadPerm(t *testing.T) {
readAction := rbac.Action("pull")
resource := rbac.Resource("/project/project_name/repository")
// secret store is null
context := NewSecurityContext("", nil)
hasReadPerm := context.HasReadPerm("project_name")
hasReadPerm := context.Can(readAction, resource)
assert.False(t, hasReadPerm)
// invalid secret
@ -106,7 +109,7 @@ func TestHasReadPerm(t *testing.T) {
secret.NewStore(map[string]string{
"jobservice_secret": secret.JobserviceUser,
}))
hasReadPerm = context.HasReadPerm("project_name")
hasReadPerm = context.Can(readAction, resource)
assert.False(t, hasReadPerm)
// valid secret, project name
@ -114,11 +117,12 @@ func TestHasReadPerm(t *testing.T) {
secret.NewStore(map[string]string{
"jobservice_secret": secret.JobserviceUser,
}))
hasReadPerm = context.HasReadPerm("project_name")
hasReadPerm = context.Can(readAction, resource)
assert.True(t, hasReadPerm)
// valid secret, project ID
hasReadPerm = context.HasReadPerm(1)
resource = rbac.Resource("/project/1/repository")
hasReadPerm = context.Can(readAction, resource)
assert.True(t, hasReadPerm)
}
@ -128,12 +132,16 @@ func TestHasWritePerm(t *testing.T) {
"secret": "username",
}))
writeAction := rbac.Action("push")
// project name
hasWritePerm := context.HasWritePerm("project_name")
resource := rbac.Resource("/project/project_name/repository")
hasWritePerm := context.Can(writeAction, resource)
assert.False(t, hasWritePerm)
// project ID
hasWritePerm = context.HasWritePerm(1)
resource = rbac.Resource("/project/1/repository")
hasWritePerm = context.Can(writeAction, resource)
assert.False(t, hasWritePerm)
}
@ -143,12 +151,16 @@ func TestHasAllPerm(t *testing.T) {
"secret": "username",
}))
allAction := rbac.Action("push+pull")
// project name
hasAllPerm := context.HasAllPerm("project_name")
resource := rbac.Resource("/project/project_name/repository")
hasAllPerm := context.Can(allAction, resource)
assert.False(t, hasAllPerm)
// project ID
hasAllPerm = context.HasAllPerm(1)
resource = rbac.Resource("/project/1/repository")
hasAllPerm = context.Can(allAction, resource)
assert.False(t, hasAllPerm)
}

View File

@ -0,0 +1,29 @@
package token
import (
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/pkg/errors"
)
// RobotClaims implements the interface of jwt.Claims
type RobotClaims struct {
jwt.StandardClaims
TokenID int64 `json:"id"`
ProjectID int64 `json:"pid"`
Access []*rbac.Policy `json:"access"`
}
// Valid valid the claims "tokenID, projectID and access".
func (rc RobotClaims) Valid() error {
if rc.TokenID < 0 {
return errors.New("Token id must an valid INT")
}
if rc.ProjectID < 0 {
return errors.New("Project id must an valid INT")
}
if rc.Access == nil {
return errors.New("The access info cannot be nil")
}
return nil
}

View File

@ -0,0 +1,68 @@
package token
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/stretchr/testify/assert"
"testing"
)
func TestValid(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &RobotClaims{
TokenID: 1,
ProjectID: 2,
Access: policies,
}
assert.Nil(t, rClaims.Valid())
}
func TestUnValidTokenID(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &RobotClaims{
TokenID: -1,
ProjectID: 2,
Access: policies,
}
assert.NotNil(t, rClaims.Valid())
}
func TestUnValidProjectID(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &RobotClaims{
TokenID: 1,
ProjectID: -2,
Access: policies,
}
assert.NotNil(t, rClaims.Valid())
}
func TestUnValidPolicy(t *testing.T) {
rClaims := &RobotClaims{
TokenID: 1,
ProjectID: 2,
Access: nil,
}
assert.NotNil(t, rClaims.Valid())
}

View File

@ -0,0 +1,85 @@
package token
import (
"crypto/ecdsa"
"crypto/rsa"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"time"
)
// HToken htoken is a jwt token for harbor robot account,
// which contains the robot ID, project ID and the access permission for the project.
// It used for authn/authz for robot account in Harbor.
type HToken struct {
jwt.Token
}
// New ...
func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) {
rClaims := &RobotClaims{
TokenID: tokenID,
ProjectID: projectID,
Access: access,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(DefaultOptions.TTL).Unix(),
Issuer: DefaultOptions.Issuer,
},
}
err := rClaims.Valid()
if err != nil {
return nil, err
}
return &HToken{
Token: *jwt.NewWithClaims(DefaultOptions.SignMethod, rClaims),
}, nil
}
// Raw get the Raw string of token
func (htk *HToken) Raw() (string, error) {
key, err := DefaultOptions.GetKey()
if err != nil {
return "", nil
}
raw, err := htk.Token.SignedString(key)
if err != nil {
log.Debugf(fmt.Sprintf("failed to issue token %v", err))
return "", err
}
return raw, err
}
// ParseWithClaims ...
func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) {
key, err := DefaultOptions.GetKey()
if err != nil {
return nil, err
}
token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != DefaultOptions.SignMethod.Alg() {
return nil, errors.New("invalid signing method")
}
switch k := key.(type) {
case *rsa.PrivateKey:
return &k.PublicKey, nil
case *ecdsa.PrivateKey:
return &k.PublicKey, nil
default:
return key, nil
}
})
if err != nil {
log.Errorf(fmt.Sprintf("parse token error, %v", err))
return nil, err
}
if !token.Valid {
log.Errorf(fmt.Sprintf("invalid jwt token, %v", token))
return nil, err
}
return &HToken{
Token: *token,
}, nil
}

View File

@ -0,0 +1,77 @@
package token
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestMain(m *testing.M) {
server, err := test.NewAdminserver(nil)
if err != nil {
panic(err)
}
defer server.Close()
if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil {
panic(err)
}
if err := config.Init(); err != nil {
panic(err)
}
result := m.Run()
if result != 0 {
os.Exit(result)
}
}
func TestNew(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
tokenID := int64(123)
projectID := int64(321)
token, err := New(tokenID, projectID, policies)
assert.Nil(t, err)
assert.Equal(t, token.Header["alg"], "RS256")
assert.Equal(t, token.Header["typ"], "JWT")
}
func TestRaw(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/library/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
tokenID := int64(123)
projectID := int64(321)
token, err := New(tokenID, projectID, policies)
assert.Nil(t, err)
rawTk, err := token.Raw()
assert.Nil(t, err)
assert.NotNil(t, rawTk)
}
func TestParseWithClaims(t *testing.T) {
rawTk := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTIzLCJQcm9qZWN0SUQiOjAsIkFjY2VzcyI6W3siUmVzb3VyY2UiOiIvcHJvamVjdC9saWJyYXkvcmVwb3NpdG9yeSIsIkFjdGlvbiI6InB1bGwiLCJFZmZlY3QiOiIifV0sIlN0YW5kYXJkQ2xhaW1zIjp7ImV4cCI6MTU0ODE0MDIyOSwiaXNzIjoiaGFyYm9yLXRva2VuLWlzc3VlciJ9fQ.Jc3qSKN4SJVUzAvBvemVpRcSOZaHlu0Avqms04qzPm4ru9-r9IRIl3mnSkI6m9XkzLUeJ7Kiwyw63ghngnVKw_PupeclOGC6s3TK5Cfmo4h-lflecXjZWwyy-dtH_e7Us_ItS-R3nXDJtzSLEpsGHCcAj-1X2s93RB2qD8LNSylvYeDezVkTzqRzzfawPJheKKh9JTrz-3eUxCwQard9-xjlwvfUYULoHTn9npNAUq4-jqhipW4uE8HL-ym33AGF57la8U0RO11hmDM5K8-PiYknbqJ_oONeS3HBNym2pEFeGjtTv2co213wl4T5lemlg4SGolMBuJ03L7_beVZ0o-MKTkKDqDwJalb6_PM-7u3RbxC9IzJMiwZKIPnD3FvV10iPxUUQHaH8Jz5UZ2pFIhi_8BNnlBfT0JOPFVYATtLjHMczZelj2YvAeR1UHBzq3E0jPpjjwlqIFgaHCaN_KMwEvadTo_Fi2sEH4pNGP7M3yehU_72oLJQgF4paJarsmEoij6ZtPs6xekBz1fccVitq_8WNIz9aeCUdkUBRwI5QKw1RdW4ua-w74ld5MZStWJA8veyoLkEb_Q9eq2oAj5KWFjJbW5-ltiIfM8gxKflsrkWAidYGcEIYcuXr7UdqEKXxtPiWM0xb3B91ovYvO5402bn3f9-UGtlcestxNHA"
rClaims := &RobotClaims{}
_, _ = ParseWithClaims(rawTk, rClaims)
assert.Equal(t, int64(123), rClaims.TokenID)
assert.Equal(t, int64(0), rClaims.ProjectID)
assert.Equal(t, "/project/libray/repository", rClaims.Access[0].Resource.String())
}

View File

@ -0,0 +1,83 @@
package token
import (
"crypto/rsa"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"io/ioutil"
"time"
)
const (
ttl = 60 * time.Minute
issuer = "harbor-token-issuer"
signedMethod = "RS256"
)
var (
privateKey = config.TokenPrivateKeyPath()
// DefaultOptions ...
DefaultOptions = NewOptions()
)
// Options ...
type Options struct {
SignMethod jwt.SigningMethod
PublicKey []byte
PrivateKey []byte
TTL time.Duration
Issuer string
}
// NewOptions ...
func NewOptions() *Options {
privateKey, err := ioutil.ReadFile(privateKey)
if err != nil {
log.Errorf(fmt.Sprintf("failed to read private key %v", err))
return nil
}
opt := &Options{
SignMethod: jwt.GetSigningMethod(signedMethod),
PrivateKey: privateKey,
Issuer: issuer,
TTL: ttl,
}
return opt
}
// GetKey ...
func (o *Options) GetKey() (interface{}, error) {
var err error
var privateKey *rsa.PrivateKey
var publicKey *rsa.PublicKey
switch o.SignMethod.(type) {
case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
if len(o.PrivateKey) > 0 {
privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(o.PrivateKey)
if err != nil {
return nil, err
}
}
if len(o.PublicKey) > 0 {
publicKey, err = jwt.ParseRSAPublicKeyFromPEM(o.PublicKey)
if err != nil {
return nil, err
}
}
if privateKey == nil {
if publicKey != nil {
return publicKey, nil
}
return nil, fmt.Errorf("key is provided")
}
if publicKey != nil && publicKey.E != privateKey.E && publicKey.N.Cmp(privateKey.N) != 0 {
return nil, fmt.Errorf("the public key and private key are not match")
}
return privateKey, nil
default:
return nil, fmt.Errorf(fmt.Sprintf("unsupported sign method, %s", o.SignMethod))
}
}

View File

@ -0,0 +1,23 @@
package token
import (
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewOptions(t *testing.T) {
defaultOpt := DefaultOptions
assert.NotNil(t, defaultOpt)
assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256"))
assert.Equal(t, defaultOpt.Issuer, "harbor-token-issuer")
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
}
func TestGetKey(t *testing.T) {
defaultOpt := DefaultOptions
key, err := defaultOpt.GetKey()
assert.Nil(t, err)
assert.NotNil(t, key)
}

View File

@ -6,7 +6,6 @@ import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/test"
@ -75,18 +74,9 @@ var adminServerDefaultConfigWithVerifyCert = map[string]interface{}{
}
func TestMain(m *testing.M) {
server, err := test.NewAdminserver(adminServerLdapTestConfig)
if err != nil {
log.Fatalf("failed to create a mock admin server: %v", err)
}
defer server.Close()
if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil {
log.Fatalf("failed to set env %s: %v", "ADMINSERVER_URL", err)
}
test.InitDatabaseFromEnv()
secretKeyPath := "/tmp/secretkey"
_, err = test.GenerateKey(secretKeyPath)
_, err := test.GenerateKey(secretKeyPath)
if err != nil {
log.Errorf("failed to generate secret key: %v", err)
return
@ -101,14 +91,7 @@ func TestMain(m *testing.M) {
log.Fatalf("failed to initialize configurations: %v", err)
}
database, err := uiConfig.Database()
if err != nil {
log.Fatalf("failed to get database configuration: %v", err)
}
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
uiConfig.Upload(adminServerLdapTestConfig)
os.Exit(m.Run())

View File

@ -17,9 +17,8 @@ import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common"
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
utilstest "github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert"
@ -27,11 +26,13 @@ import (
"os"
"path"
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/utils/log"
)
var endpoint = "10.117.4.142"
var notaryServer *httptest.Server
var adminServer *httptest.Server
func TestMain(m *testing.M) {
notaryServer = notarytest.NewNotaryServer(endpoint)
@ -42,17 +43,12 @@ func TestMain(m *testing.M) {
common.CfgExpiration: 5,
common.TokenExpiration: 30,
}
adminServer, err := utilstest.NewAdminserver(defaultConfig)
if err != nil {
panic(err)
}
defer adminServer.Close()
if err := os.Setenv("ADMINSERVER_URL", adminServer.URL); err != nil {
panic(err)
}
if err := config.Init(); err != nil {
panic(err)
log.Fatalf("failed to initialize config: %v", err)
}
test.InitDatabaseFromEnv()
config.Upload(defaultConfig)
notaryCachePath = "/tmp/notary"
result := m.Run()
if result != 0 {

View File

@ -15,11 +15,13 @@
package test
import (
"fmt"
"os"
"strconv"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
)
@ -44,6 +46,7 @@ func InitDatabaseFromEnv() {
dbPassword := os.Getenv("POSTGRESQL_PWD")
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
adminPwd := os.Getenv("HARBOR_ADMIN_PASSWD")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
}
@ -61,7 +64,32 @@ func InitDatabaseFromEnv() {
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
if err := dao.InitAndUpgradeDatabase(database); err != nil {
log.Fatalf("failed to init and upgrade database : %v", err)
}
if err := updateUserInitialPassword(1, adminPwd); err != nil {
log.Fatalf("failed to init password for admin: %v", err)
}
}
func updateUserInitialPassword(userID int, password string) error {
queryUser := models.User{UserID: userID}
user, err := dao.GetUser(queryUser)
if err != nil {
return fmt.Errorf("Failed to get user, userID: %d %v", userID, err)
}
if user == nil {
return fmt.Errorf("user id: %d does not exist", userID)
}
if user.Salt == "" {
user.Salt = utils.GenerateRandomString()
user.Password = password
err = dao.ChangeUserPassword(*user)
if err != nil {
return fmt.Errorf("Failed to update user encrypted password, userID: %d, err: %v", userID, err)
}
} else {
}
return nil
}

View File

@ -21,7 +21,11 @@ import (
"net/http/httptest"
"strings"
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/gorilla/mux"
"os"
"sort"
)
// RequestHandlerMapping is a mapping between request and its handler
@ -87,3 +91,54 @@ func NewServer(mappings ...*RequestHandlerMapping) *httptest.Server {
return httptest.NewServer(r)
}
// GetUnitTestConfig ...
func GetUnitTestConfig() map[string]interface{} {
ipAddress := os.Getenv("IP")
return map[string]interface{}{
common.ExtEndpoint: fmt.Sprintf("https://%s", ipAddress),
common.AUTHMode: "db_auth",
common.DatabaseType: "postgresql",
common.PostGreSQLHOST: ipAddress,
common.PostGreSQLPort: 5432,
common.PostGreSQLUsername: "postgres",
common.PostGreSQLPassword: "root123",
common.PostGreSQLDatabase: "registry",
common.LDAPURL: "ldap://ldap.vmware.com",
common.LDAPSearchDN: "cn=admin,dc=example,dc=com",
common.LDAPSearchPwd: "admin",
common.LDAPBaseDN: "dc=example,dc=com",
common.LDAPUID: "uid",
common.LDAPFilter: "",
common.LDAPScope: 2,
common.LDAPTimeout: 30,
common.LDAPVerifyCert: true,
common.UAAVerifyCert: true,
common.ClairDBHost: "postgresql",
common.CfgExpiration: 5,
common.AdminInitialPassword: "Harbor12345",
common.LDAPGroupSearchFilter: "objectclass=groupOfNames",
common.LDAPGroupBaseDN: "dc=example,dc=com",
common.LDAPGroupAttributeName: "cn",
common.LDAPGroupSearchScope: 2,
common.LdapGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com",
common.WithNotary: "false",
common.WithChartMuseum: "false",
common.SelfRegistration: "true",
common.WithClair: "false",
common.TokenServiceURL: "http://core:8080/service/token",
common.RegistryURL: fmt.Sprintf("http://%s:5000", ipAddress),
}
}
// TraceCfgMap ...
func TraceCfgMap(cfgs map[string]interface{}) {
var keys []string
for k := range cfgs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%v=%v\n", k, cfgs[k])
}
}

View File

@ -218,3 +218,19 @@ func ParseOfftime(offtime int64) (hour, minite, second int) {
func TrimLower(str string) string {
return strings.TrimSpace(strings.ToLower(str))
}
// GetStrValueOfAnyType return string format of any value, for map, need to convert to json
func GetStrValueOfAnyType(value interface{}) string {
var strVal string
if _, ok := value.(map[string]interface{}); ok {
b, err := json.Marshal(value)
if err != nil {
log.Errorf("can not marshal json object, error %v", err)
return ""
}
strVal = string(b)
} else {
strVal = fmt.Sprintf("%v", value)
}
return strVal
}

View File

@ -40,8 +40,8 @@ import (
)
var (
nonSysAdminID, projAdminID, projDeveloperID, projGuestID int64
projAdminPMID, projDeveloperPMID, projGuestPMID int
nonSysAdminID, projAdminID, projDeveloperID, projGuestID, projAdminRobotID int64
projAdminPMID, projDeveloperPMID, projGuestPMID, projAdminRobotPMID int
// The following users/credentials are registered and assigned roles at the beginning of
// running testing and cleaned up at the end.
// Do not try to change the system and project roles that the users have during
@ -67,6 +67,10 @@ var (
Name: "proj_guest",
Passwd: "Harbor12345",
}
projAdmin4Robot = &usrInfo{
Name: "proj_admin_robot",
Passwd: "Harbor12345",
}
)
type testingRequest struct {
@ -240,6 +244,25 @@ func prepare() error {
return err
}
// register projAdminRobots and assign project admin role
projAdminRobotID, err = dao.Register(models.User{
Username: projAdmin4Robot.Name,
Password: projAdmin4Robot.Passwd,
Email: projAdmin4Robot.Name + "@test.com",
})
if err != nil {
return err
}
if projAdminRobotPMID, err = project.AddProjectMember(models.Member{
ProjectID: 1,
Role: models.PROJECTADMIN,
EntityID: int(projAdminRobotID),
EntityType: common.UserMember,
}); err != nil {
return err
}
// register projDeveloper and assign project developer role
projDeveloperID, err = dao.Register(models.User{
Username: projDeveloper.Name,

View File

@ -125,6 +125,7 @@ func (b *BaseController) WriteYamlData(object interface{}) {
// Init related objects/configurations for the API controllers
func Init() error {
registerHealthCheckers()
// If chart repository is not enabled then directly return
if !config.WithChartMuseum() {
return nil

View File

@ -6,6 +6,7 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
)
const (
@ -45,12 +46,6 @@ func (cla *ChartLabelAPI) Prepare() {
}
cla.project = existingProject
// Check permission
if !cla.checkPermissions(project) {
cla.SendForbiddenError(errors.New(cla.SecurityCtx.GetUsername()))
return
}
// Check the existence of target chart
chartName := cla.GetStringFromPath(nameParam)
version := cla.GetStringFromPath(versionParam)
@ -62,8 +57,23 @@ func (cla *ChartLabelAPI) Prepare() {
cla.chartFullName = fmt.Sprintf("%s/%s:%s", project, chartName, version)
}
func (cla *ChartLabelAPI) requireAccess(action rbac.Action) bool {
resource := rbac.NewProjectNamespace(cla.project.ProjectID).Resource(rbac.ResourceHelmChartVersionLabel)
if !cla.SecurityCtx.Can(action, resource) {
cla.HandleForbidden(cla.SecurityCtx.GetUsername())
return false
}
return true
}
// MarkLabel handles the request of marking label to chart.
func (cla *ChartLabelAPI) MarkLabel() {
if !cla.requireAccess(rbac.ActionCreate) {
return
}
l := &models.Label{}
cla.DecodeJSONReq(l)
@ -83,6 +93,10 @@ func (cla *ChartLabelAPI) MarkLabel() {
// RemoveLabel handles the request of removing label from chart.
func (cla *ChartLabelAPI) RemoveLabel() {
if !cla.requireAccess(rbac.ActionDelete) {
return
}
lID, err := cla.GetInt64FromPath(idParam)
if err != nil {
cla.SendInternalServerError(err)

View File

@ -15,6 +15,7 @@ import (
"github.com/goharbor/harbor/src/core/label"
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/rbac"
hlog "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
)
@ -84,10 +85,35 @@ func (cra *ChartRepositoryAPI) Prepare() {
cra.labelManager = &label.BaseManager{}
}
func (cra *ChartRepositoryAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
if len(subresource) == 0 {
subresource = append(subresource, rbac.ResourceHelmChart)
}
resource := rbac.NewProjectNamespace(cra.namespace).Resource(subresource...)
if !cra.SecurityCtx.Can(action, resource) {
if !cra.SecurityCtx.IsAuthenticated() {
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
} else {
cra.HandleForbidden(cra.SecurityCtx.GetUsername())
}
return false
}
return true
}
// GetHealthStatus handles GET /api/chartrepo/health
func (cra *ChartRepositoryAPI) GetHealthStatus() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelSystem) {
if !cra.SecurityCtx.IsAuthenticated() {
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
if !cra.SecurityCtx.IsSysAdmin() {
cra.HandleForbidden(cra.SecurityCtx.GetUsername())
return
}
@ -98,7 +124,7 @@ func (cra *ChartRepositoryAPI) GetHealthStatus() {
// GetIndexByRepo handles GET /:repo/index.yaml
func (cra *ChartRepositoryAPI) GetIndexByRepo() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelRead) {
if !cra.requireAccess(rbac.ActionRead) {
return
}
@ -109,7 +135,13 @@ func (cra *ChartRepositoryAPI) GetIndexByRepo() {
// GetIndex handles GET /index.yaml
func (cra *ChartRepositoryAPI) GetIndex() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelSystem) {
if !cra.SecurityCtx.IsAuthenticated() {
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
if !cra.SecurityCtx.IsSysAdmin() {
cra.HandleForbidden(cra.SecurityCtx.GetUsername())
return
}
@ -136,7 +168,7 @@ func (cra *ChartRepositoryAPI) GetIndex() {
// DownloadChart handles GET /:repo/charts/:filename
func (cra *ChartRepositoryAPI) DownloadChart() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelRead) {
if !cra.requireAccess(rbac.ActionRead) {
return
}
@ -147,7 +179,7 @@ func (cra *ChartRepositoryAPI) DownloadChart() {
// ListCharts handles GET /api/:repo/charts
func (cra *ChartRepositoryAPI) ListCharts() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelRead) {
if !cra.requireAccess(rbac.ActionList) {
return
}
@ -163,7 +195,7 @@ func (cra *ChartRepositoryAPI) ListCharts() {
// ListChartVersions GET /api/:repo/charts/:name
func (cra *ChartRepositoryAPI) ListChartVersions() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelRead) {
if !cra.requireAccess(rbac.ActionList, rbac.ResourceHelmChartVersion) {
return
}
@ -191,7 +223,7 @@ func (cra *ChartRepositoryAPI) ListChartVersions() {
// GetChartVersion handles GET /api/:repo/charts/:name/:version
func (cra *ChartRepositoryAPI) GetChartVersion() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelRead) {
if !cra.requireAccess(rbac.ActionRead, rbac.ResourceHelmChartVersion) {
return
}
@ -219,7 +251,7 @@ func (cra *ChartRepositoryAPI) GetChartVersion() {
// DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version
func (cra *ChartRepositoryAPI) DeleteChartVersion() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelAll) {
if !cra.requireAccess(rbac.ActionDelete, rbac.ResourceHelmChartVersion) {
return
}
@ -244,7 +276,7 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() {
hlog.Debugf("Header of request of uploading chart: %#v, content-len=%d", cra.Ctx.Request.Header, cra.Ctx.Request.ContentLength)
// Check access
if !cra.requireAccess(cra.namespace, accessLevelWrite) {
if !cra.requireAccess(rbac.ActionCreate, rbac.ResourceHelmChartVersion) {
return
}
@ -272,7 +304,7 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() {
// UploadChartProvFile handles POST /api/:repo/prov
func (cra *ChartRepositoryAPI) UploadChartProvFile() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelWrite) {
if !cra.requireAccess(rbac.ActionCreate) {
return
}
@ -297,7 +329,7 @@ func (cra *ChartRepositoryAPI) UploadChartProvFile() {
// DeleteChart deletes all the chart versions of the specified chart.
func (cra *ChartRepositoryAPI) DeleteChart() {
// Check access
if !cra.requireAccess(cra.namespace, accessLevelWrite) {
if !cra.requireAccess(rbac.ActionDelete) {
return
}
@ -365,62 +397,6 @@ func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
return true
}
// Check if the related access match the expected requirement
// If with right access, return true
// If without right access, return false
func (cra *ChartRepositoryAPI) requireAccess(namespace string, accessLevel uint) bool {
if accessLevel == accessLevelPublic {
return true // do nothing
}
theLevel := accessLevel
// If repo is empty, system admin role must be required
if len(namespace) == 0 {
theLevel = accessLevelSystem
}
var err error
switch theLevel {
// Should be system admin role
case accessLevelSystem:
if !cra.SecurityCtx.IsSysAdmin() {
err = errors.New("permission denied: system admin role is required")
}
case accessLevelAll:
if !cra.SecurityCtx.HasAllPerm(namespace) {
err = errors.New("permission denied: project admin or higher role is required")
}
case accessLevelWrite:
if !cra.SecurityCtx.HasWritePerm(namespace) {
err = errors.New("permission denied: developer or higher role is required")
}
case accessLevelRead:
if !cra.SecurityCtx.HasReadPerm(namespace) {
err = errors.New("permission denied: guest or higher role is required")
}
default:
// access rejected for invalid scope
cra.SendForbiddenError(errors.New("unrecognized access scope"))
return false
}
// Access is not granted, check if user has authenticated
if err != nil {
// Unauthenticated, return 401
if !cra.SecurityCtx.IsAuthenticated() {
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
return false
}
// Authenticated, return 403
cra.SendForbiddenError(err)
return false
}
return true
}
// formFile is used to represent the uploaded files in the form
type formFile struct {
// form field key contains the form file

View File

@ -8,6 +8,7 @@ import (
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/promgr/metamgr"
)
@ -16,29 +17,6 @@ var (
crMockServer *httptest.Server
)
// Test access checking
func TestRequireAccess(t *testing.T) {
chartAPI := &ChartRepositoryAPI{}
chartAPI.SecurityCtx = &mockSecurityContext{}
ns := "library"
if !chartAPI.requireAccess(ns, accessLevelPublic) {
t.Fatal("expect true result (public access level is granted) but got false")
}
if !chartAPI.requireAccess(ns, accessLevelAll) {
t.Fatal("expect true result (admin has all perm) but got false")
}
if !chartAPI.requireAccess(ns, accessLevelRead) {
t.Fatal("expect true result (admin has read perm) but got false")
}
if !chartAPI.requireAccess(ns, accessLevelWrite) {
t.Fatal("expect true result (admin has write perm) but got false")
}
if !chartAPI.requireAccess(ns, accessLevelSystem) {
t.Fatal("expect true result (admin has system perm) but got false")
}
}
func TestIsMultipartFormData(t *testing.T) {
req, err := createRequest(http.MethodPost, "/api/chartrepo/charts")
if err != nil {
@ -204,7 +182,7 @@ func TestDeleteChart(t *testing.T) {
request: &testingRequest{
url: "/api/chartrepo/library/charts/harbor",
method: http.MethodDelete,
credential: projDeveloper,
credential: projAdmin,
},
code: http.StatusOK,
})
@ -309,8 +287,15 @@ func (msc *mockSecurityContext) IsSolutionUser() bool {
return false
}
// HasReadPerm returns whether the user has read permission to the project
func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
// Can returns whether the user can do action on resource
func (msc *mockSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
namespace, err := resource.GetNamespace()
if err != nil || namespace.Kind() != "project" {
return false
}
projectIDOrName := namespace.Identity()
if projectIDOrName == nil {
return false
}
@ -324,26 +309,6 @@ func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
return false
}
// HasWritePerm returns whether the user has write permission to the project
func (msc *mockSecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
if projectIDOrName == nil {
return false
}
if ns, ok := projectIDOrName.(string); ok {
if ns == "library" {
return true
}
}
return false
}
// HasAllPerm returns whether the user has all permissions to the project
func (msc *mockSecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
return msc.HasReadPerm(projectIDOrName) && msc.HasWritePerm(projectIDOrName)
}
// Get current user's all project
func (msc *mockSecurityContext) GetMyProjects() ([]*models.Project, error) {
return []*models.Project{{ProjectID: 0, Name: "library"}}, nil

View File

@ -17,31 +17,47 @@ package api
import (
"fmt"
"net/http"
"reflect"
"strings"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/config"
"github.com/goharbor/harbor/src/common/config/metadata"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/security/secret"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
corecfg "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/filter"
)
// ConfigAPI ...
type ConfigAPI struct {
BaseController
cfgManager *config.CfgManager
}
// Prepare validates the user
func (c *ConfigAPI) Prepare() {
c.BaseController.Prepare()
c.cfgManager = corecfg.GetCfgManager()
if !c.SecurityCtx.IsAuthenticated() {
c.HandleUnauthorized()
return
}
// Only internal container can access /api/internal/configurations
if strings.EqualFold(c.Ctx.Request.RequestURI, "/api/internal/configurations") {
if _, ok := c.Ctx.Request.Context().Value(filter.SecurCtxKey).(*secret.SecurityContext); !ok {
c.HandleUnauthorized()
return
}
}
if !c.SecurityCtx.IsSysAdmin() && !c.SecurityCtx.IsSolutionUser() {
c.HandleForbidden(c.SecurityCtx.GetUsername())
return
}
}
type value struct {
@ -51,20 +67,8 @@ type value struct {
// Get returns configurations
func (c *ConfigAPI) Get() {
configs, err := config.GetSystemCfg()
if err != nil {
log.Errorf("failed to get configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
cfgs := map[string]interface{}{}
for _, k := range common.HarborValidKeys {
if v, ok := configs[k]; ok {
cfgs[k] = v
}
}
m, err := convertForGet(cfgs)
configs := c.cfgManager.GetUserCfgs()
m, err := convertForGet(configs)
if err != nil {
log.Errorf("failed to convert configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
@ -76,11 +80,8 @@ func (c *ConfigAPI) Get() {
// GetInternalConfig returns internal configurations
func (c *ConfigAPI) GetInternalConfig() {
configs, err := config.GetSystemCfg()
if err != nil {
log.Errorf("failed to get configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
configs := c.cfgManager.GetAll()
c.Data["json"] = configs
c.ServeJSON()
}
@ -89,16 +90,12 @@ func (c *ConfigAPI) GetInternalConfig() {
func (c *ConfigAPI) Put() {
m := map[string]interface{}{}
c.DecodeJSONReq(&m)
cfg := map[string]interface{}{}
for _, k := range common.HarborValidKeys {
if v, ok := m[k]; ok {
cfg[k] = v
}
err := c.cfgManager.Load()
if err != nil {
log.Errorf("failed to get configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
isSysErr, err := validateCfg(cfg)
isSysErr, err := c.validateCfg(m)
if err != nil {
if isSysErr {
log.Errorf("failed to validate configurations: %v", err)
@ -108,146 +105,26 @@ func (c *ConfigAPI) Put() {
c.CustomAbort(http.StatusBadRequest, err.Error())
}
if err := config.Upload(cfg); err != nil {
if err := c.cfgManager.UpdateConfig(m); err != nil {
log.Errorf("failed to upload configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if err := config.Load(); err != nil {
log.Errorf("failed to load configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
// Everything is ok, detect the configurations to confirm if the option we are caring is changed.
if err := watchConfigChanges(cfg); err != nil {
log.Errorf("Failed to watch configuration change with error: %s\n", err)
}
}
// Reset system configurations
func (c *ConfigAPI) Reset() {
if err := config.Reset(); err != nil {
log.Errorf("failed to reset configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}
func validateCfg(c map[string]interface{}) (bool, error) {
strMap := map[string]string{}
for k := range common.HarborStringKeysMap {
if _, ok := c[k]; !ok {
continue
}
if _, ok := c[k].(string); !ok {
return false, fmt.Errorf("Invalid value type, expected string, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
}
strMap[k] = c[k].(string)
}
numMap := map[string]int{}
for k := range common.HarborNumKeysMap {
if _, ok := c[k]; !ok {
continue
}
if _, ok := c[k].(float64); !ok {
return false, fmt.Errorf("Invalid value type, expected float64, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
}
numMap[k] = int(c[k].(float64))
}
boolMap := map[string]bool{}
for k := range common.HarborBoolKeysMap {
if _, ok := c[k]; !ok {
continue
}
if _, ok := c[k].(bool); !ok {
return false, fmt.Errorf("Invalid value type, expected bool, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
}
boolMap[k] = c[k].(bool)
}
mode, err := config.AuthMode()
if err != nil {
return true, err
}
if value, ok := strMap[common.AUTHMode]; ok {
if value != common.DBAuth && value != common.LDAPAuth && value != common.UAAAuth {
return false, fmt.Errorf("invalid %s, shoud be one of %s, %s, %s", common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth)
}
func (c *ConfigAPI) validateCfg(cfgs map[string]interface{}) (bool, error) {
mode := c.cfgManager.Get("auth_mode").GetString()
if value, ok := cfgs[common.AUTHMode]; ok {
flag, err := authModeCanBeModified()
if err != nil {
return true, err
}
if mode != value && !flag {
if mode != fmt.Sprintf("%v", value) && !flag {
return false, fmt.Errorf("%s can not be modified as new users have been inserted into database", common.AUTHMode)
}
mode = value
}
if mode == common.LDAPAuth {
ldapConf, err := config.LDAPConf()
if err != nil {
return true, err
}
if len(ldapConf.LdapURL) == 0 {
if _, ok := strMap[common.LDAPURL]; !ok {
return false, fmt.Errorf("%s is missing", common.LDAPURL)
}
}
if len(ldapConf.LdapBaseDn) == 0 {
if _, ok := strMap[common.LDAPBaseDN]; !ok {
return false, fmt.Errorf("%s is missing", common.LDAPBaseDN)
}
}
if len(ldapConf.LdapUID) == 0 {
if _, ok := strMap[common.LDAPUID]; !ok {
return false, fmt.Errorf("%s is missing", common.LDAPUID)
}
}
if ldapConf.LdapScope == 0 {
if _, ok := numMap[common.LDAPScope]; !ok {
return false, fmt.Errorf("%s is missing", common.LDAPScope)
}
}
}
if ldapURL, ok := strMap[common.LDAPURL]; ok && len(ldapURL) == 0 {
return false, fmt.Errorf("%s is empty", common.LDAPURL)
}
if baseDN, ok := strMap[common.LDAPBaseDN]; ok && len(baseDN) == 0 {
return false, fmt.Errorf("%s is empty", common.LDAPBaseDN)
}
if uID, ok := strMap[common.LDAPUID]; ok && len(uID) == 0 {
return false, fmt.Errorf("%s is empty", common.LDAPUID)
}
if scope, ok := numMap[common.LDAPScope]; ok &&
scope != common.LDAPScopeBase &&
scope != common.LDAPScopeOnelevel &&
scope != common.LDAPScopeSubtree {
return false, fmt.Errorf("invalid %s, should be %d, %d or %d",
common.LDAPScope,
common.LDAPScopeBase,
common.LDAPScopeOnelevel,
common.LDAPScopeSubtree)
}
for k, n := range numMap {
if n < 0 {
return false, fmt.Errorf("invalid %s: %d", k, n)
}
if (k == common.EmailPort ||
k == common.PostGreSQLPort) && n > 65535 {
return false, fmt.Errorf("invalid %s: %d", k, n)
}
}
if crt, ok := strMap[common.ProjectCreationRestriction]; ok &&
crt != common.ProCrtRestrEveryone &&
crt != common.ProCrtRestrAdmOnly {
return false, fmt.Errorf("invalid %s, should be %s or %s",
common.ProjectCreationRestriction,
common.ProCrtRestrAdmOnly,
common.ProCrtRestrEveryone)
err := c.cfgManager.ValidateCfg(cfgs)
if err != nil {
return false, err
}
return false, nil
}
@ -256,9 +133,11 @@ func validateCfg(c map[string]interface{}) (bool, error) {
func convertForGet(cfg map[string]interface{}) (map[string]*value, error) {
result := map[string]*value{}
for _, k := range common.HarborPasswordKeys {
if _, ok := cfg[k]; ok {
delete(cfg, k)
mList := metadata.Instance().GetAll()
for _, item := range mList {
if _, ok := item.ItemType.(*metadata.PasswordType); ok {
delete(cfg, item.Name)
}
}

View File

@ -45,7 +45,7 @@ func TestGetConfig(t *testing.T) {
if !assert.Equal(200, code, "the status code of getting configurations with admin user should be 200") {
return
}
t.Logf("cfg: %+v", cfg)
mode := cfg[common.AUTHMode].Value.(string)
assert.Equal(common.DBAuth, mode, fmt.Sprintf("the auth mode should be %s", common.DBAuth))
ccc, err := config.GetSystemCfg()
@ -103,46 +103,6 @@ func TestPutConfig(t *testing.T) {
t.Logf("%v", ccc)
}
func TestResetConfig(t *testing.T) {
fmt.Println("Testing resetting configurations")
assert := assert.New(t)
apiTest := newHarborAPI()
code, err := apiTest.ResetConfig(*admin)
if err != nil {
t.Errorf("failed to get configurations: %v", err)
return
}
if !assert.Equal(200, code, "unexpected response code") {
return
}
code, cfgs, err := apiTest.GetConfig(*admin)
if err != nil {
t.Errorf("failed to get configurations: %v", err)
return
}
if !assert.Equal(200, code, "unexpected response code") {
return
}
value, ok := cfgs[common.TokenExpiration]
if !ok {
t.Errorf("%s not found", common.TokenExpiration)
return
}
assert.Equal(int(value.Value.(float64)), 30, "unexpected 30")
ccc, err := config.GetSystemCfg()
if err != nil {
t.Logf("failed to get system configurations: %v", err)
}
t.Logf("%v", ccc)
}
func TestPutConfigMaxLength(t *testing.T) {
fmt.Println("Testing modifying configurations with max length.")
assert := assert.New(t)

View File

@ -106,7 +106,7 @@ func CommonDelTarget() {
func CommonAddRepository() {
commonRepository := &models.RepoRecord{
RepositoryID: 1,
RepositoryID: 31,
Name: TestRepoName,
ProjectID: 1,
PullCount: 1,

View File

@ -27,20 +27,20 @@ import (
"runtime"
"strconv"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/job/test"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
testutils "github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/tests/apitests/apilib"
// "strconv"
// "strings"
"github.com/astaxie/beego"
"github.com/dghubble/sling"
// for test env prepare
"github.com/goharbor/harbor/src/common/dao"
_ "github.com/goharbor/harbor/src/core/auth/db"
_ "github.com/goharbor/harbor/src/core/auth/ldap"
"github.com/goharbor/harbor/src/replication/core"
@ -78,14 +78,14 @@ type usrInfo struct {
}
func init() {
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
database, err := config.Database()
if err != nil {
log.Fatalf("failed to get database configurations: %v", err)
}
dao.InitDatabase(database)
config.Init()
testutils.InitDatabaseFromEnv()
dao.PrepareTestData([]string{"delete from harbor_user where user_id >2", "delete from project where owner_id >2"}, []string{})
config.Upload(testutils.GetUnitTestConfig())
allCfgs, _ := config.GetSystemCfg()
testutils.TraceCfgMap(allCfgs)
_, file, _, _ := runtime.Caller(0)
dir := filepath.Dir(file)
dir = filepath.Join(dir, "..")
@ -96,12 +96,14 @@ func init() {
filter.Init()
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
beego.Router("/api/search/", &SearchAPI{})
beego.Router("/api/projects/", &ProjectAPI{}, "get:List;post:Post;head:Head")
beego.Router("/api/projects/:id", &ProjectAPI{}, "delete:Delete;get:Get;put:Put")
beego.Router("/api/users/:id", &UserAPI{}, "get:Get")
beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put")
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions")
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs")
beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable")
@ -140,7 +142,6 @@ func init() {
beego.Router("/api/ldap/groups/search", &LdapAPI{}, "get:SearchGroup")
beego.Router("/api/ldap/users/import", &LdapAPI{}, "post:ImportUser")
beego.Router("/api/configurations", &ConfigAPI{})
beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset")
beego.Router("/api/configs", &ConfigAPI{}, "get:GetInternalConfig")
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
beego.Router("/api/replications", &ReplicationAPI{})
@ -152,6 +153,9 @@ func init() {
beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog")
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
// Charts are controlled under projects
chartRepositoryAPIType := &ChartRepositoryAPI{}
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
@ -173,8 +177,6 @@ func init() {
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
_ = updateInitPassword(1, "Harbor12345")
if err := core.Init(); err != nil {
log.Fatalf("failed to initialize GlobalController: %v", err)
}
@ -992,6 +994,23 @@ func (a testapi) UsersUpdatePassword(userID int, password apilib.Password, authI
return httpStatusCode, err
}
func (a testapi) UsersGetPermissions(userID interface{}, scope string, authInfo usrInfo) (int, []apilib.Permission, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := fmt.Sprintf("/api/users/%v/permissions", userID)
_sling = _sling.Path(path)
type QueryParams struct {
Scope string `url:"scope,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{Scope: scope})
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
var successPayLoad []apilib.Permission
if 200 == httpStatusCode && nil == err {
err = json.Unmarshal(body, &successPayLoad)
}
return httpStatusCode, successPayLoad, err
}
// Mark a registered user as be removed.
func (a testapi) UsersDelete(userID int, authInfo usrInfo) (int, error) {
_sling := sling.New().Delete(a.basePath)
@ -1001,27 +1020,6 @@ func (a testapi) UsersDelete(userID int, authInfo usrInfo) (int, error) {
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
func updateInitPassword(userID int, password string) error {
queryUser := models.User{UserID: userID}
user, err := dao.GetUser(queryUser)
if err != nil {
return fmt.Errorf("Failed to get user, userID: %d %v", userID, err)
}
if user == nil {
return fmt.Errorf("user id: %d does not exist", userID)
}
if user.Salt == "" {
user.Salt = utils.GenerateRandomString()
user.Password = password
err = dao.ChangeUserPassword(*user)
if err != nil {
return fmt.Errorf("Failed to update user encrypted password, userID: %d, err: %v", userID, err)
}
} else {
}
return nil
}
// Get system volume info
func (a testapi) VolumeInfoGet(authInfo usrInfo) (int, apilib.SystemInfo, error) {

323
src/core/api/health.go Normal file
View File

@ -0,0 +1,323 @@
// Copyright 2019 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/dao"
httputil "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/docker/distribution/health"
"github.com/gomodule/redigo/redis"
)
var (
timeout = 60 * time.Second
healthCheckerRegistry = map[string]health.Checker{}
)
type overallHealthStatus struct {
Status string `json:"status"`
Components []*componentHealthStatus `json:"components"`
}
type componentHealthStatus struct {
Name string `json:"name"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
}
type healthy bool
func (h healthy) String() string {
if h {
return "healthy"
}
return "unhealthy"
}
// HealthAPI handles the request for "/api/health"
type HealthAPI struct {
BaseController
}
// CheckHealth checks the health of system
func (h *HealthAPI) CheckHealth() {
var isHealthy healthy = true
components := []*componentHealthStatus{}
c := make(chan *componentHealthStatus, len(healthCheckerRegistry))
for name, checker := range healthCheckerRegistry {
go check(name, checker, timeout, c)
}
for i := 0; i < len(healthCheckerRegistry); i++ {
componentStatus := <-c
if len(componentStatus.Error) != 0 {
isHealthy = false
}
components = append(components, componentStatus)
}
status := &overallHealthStatus{}
status.Status = isHealthy.String()
status.Components = components
if !isHealthy {
log.Debugf("unhealthy system status: %v", status)
}
h.WriteJSONData(status)
}
func check(name string, checker health.Checker,
timeout time.Duration, c chan *componentHealthStatus) {
statusChan := make(chan *componentHealthStatus)
go func() {
err := checker.Check()
var healthy healthy = err == nil
status := &componentHealthStatus{
Name: name,
Status: healthy.String(),
}
if !healthy {
status.Error = err.Error()
}
statusChan <- status
}()
select {
case status := <-statusChan:
c <- status
case <-time.After(timeout):
var healthy healthy = false
c <- &componentHealthStatus{
Name: name,
Status: healthy.String(),
Error: "failed to check the health status: timeout",
}
}
}
// HTTPStatusCodeHealthChecker implements a Checker to check that the HTTP status code
// returned matches the expected one
func HTTPStatusCodeHealthChecker(method string, url string, header http.Header,
timeout time.Duration, statusCode int) health.Checker {
return health.CheckFunc(func() error {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %v", err)
}
for key, values := range header {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := httputil.NewClient(&http.Client{
Timeout: timeout,
})
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to check health: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != statusCode {
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Debugf("failed to read response body: %v", err)
}
return fmt.Errorf("received unexpected status code: %d %s", resp.StatusCode, string(data))
}
return nil
})
}
type updater struct {
sync.Mutex
status error
}
func (u *updater) Check() error {
u.Lock()
defer u.Unlock()
return u.status
}
func (u *updater) update(status error) {
u.Lock()
defer u.Unlock()
u.status = status
}
// PeriodicHealthChecker implements a Checker to check status periodically
func PeriodicHealthChecker(checker health.Checker, period time.Duration) health.Checker {
u := &updater{
// init the "status" as "unknown status" error to avoid returning nil error(which means healthy)
// before the first health check request finished
status: errors.New("unknown status"),
}
go func() {
ticker := time.NewTicker(period)
for {
u.update(checker.Check())
<-ticker.C
}
}()
return u
}
func coreHealthChecker() health.Checker {
return health.CheckFunc(func() error {
return nil
})
}
func portalHealthChecker() health.Checker {
url := config.GetPortalURL()
timeout := 60 * time.Second
period := 10 * time.Second
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
return PeriodicHealthChecker(checker, period)
}
func jobserviceHealthChecker() health.Checker {
url := config.InternalJobServiceURL() + "/api/v1/stats"
timeout := 60 * time.Second
period := 10 * time.Second
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
return PeriodicHealthChecker(checker, period)
}
func registryHealthChecker() health.Checker {
url := getRegistryURL() + "/v2"
timeout := 60 * time.Second
period := 10 * time.Second
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusUnauthorized)
return PeriodicHealthChecker(checker, period)
}
func registryCtlHealthChecker() health.Checker {
url := config.GetRegistryCtlURL() + "/api/health"
timeout := 60 * time.Second
period := 10 * time.Second
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
return PeriodicHealthChecker(checker, period)
}
func chartmuseumHealthChecker() health.Checker {
url, err := config.GetChartMuseumEndpoint()
if err != nil {
log.Errorf("failed to get the URL of chartmuseum: %v", err)
}
url = url + "/health"
timeout := 60 * time.Second
period := 10 * time.Second
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
return PeriodicHealthChecker(checker, period)
}
func clairHealthChecker() health.Checker {
url := config.GetClairHealthCheckServerURL() + "/health"
timeout := 60 * time.Second
period := 10 * time.Second
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
return PeriodicHealthChecker(checker, period)
}
func notaryHealthChecker() health.Checker {
url := config.InternalNotaryEndpoint() + "/_notary_server/health"
timeout := 60 * time.Second
period := 10 * time.Second
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
return PeriodicHealthChecker(checker, period)
}
func databaseHealthChecker() health.Checker {
period := 10 * time.Second
checker := health.CheckFunc(func() error {
_, err := dao.GetOrmer().Raw("SELECT 1").Exec()
if err != nil {
return fmt.Errorf("failed to run SQL \"SELECT 1\": %v", err)
}
return nil
})
return PeriodicHealthChecker(checker, period)
}
func redisHealthChecker() health.Checker {
url := config.GetRedisOfRegURL()
timeout := 60 * time.Second
period := 10 * time.Second
checker := health.CheckFunc(func() error {
conn, err := redis.DialURL(url,
redis.DialConnectTimeout(timeout*time.Second),
redis.DialReadTimeout(timeout*time.Second),
redis.DialWriteTimeout(timeout*time.Second))
if err != nil {
return fmt.Errorf("failed to establish connection with Redis: %v", err)
}
defer conn.Close()
_, err = conn.Do("PING")
if err != nil {
return fmt.Errorf("failed to run \"PING\": %v", err)
}
return nil
})
return PeriodicHealthChecker(checker, period)
}
func registerHealthCheckers() {
healthCheckerRegistry["core"] = coreHealthChecker()
healthCheckerRegistry["portal"] = portalHealthChecker()
healthCheckerRegistry["jobservice"] = jobserviceHealthChecker()
healthCheckerRegistry["registry"] = registryHealthChecker()
healthCheckerRegistry["registryctl"] = registryCtlHealthChecker()
healthCheckerRegistry["database"] = databaseHealthChecker()
healthCheckerRegistry["redis"] = redisHealthChecker()
if config.WithChartMuseum() {
healthCheckerRegistry["chartmuseum"] = chartmuseumHealthChecker()
}
if config.WithClair() {
healthCheckerRegistry["clair"] = clairHealthChecker()
}
if config.WithNotary() {
healthCheckerRegistry["notary"] = notaryHealthChecker()
}
}
func getRegistryURL() string {
endpoint, err := config.RegistryURL()
if err != nil {
log.Errorf("failed to get the URL of registry: %v", err)
return ""
}
url, err := utils.ParseEndpoint(endpoint)
if err != nil {
log.Errorf("failed to parse the URL of registry: %v", err)
return ""
}
return url.String()
}

134
src/core/api/health_test.go Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2019 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"errors"
"net/http"
"testing"
"time"
"github.com/docker/distribution/health"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStringOfHealthy(t *testing.T) {
var isHealthy healthy = true
assert.Equal(t, "healthy", isHealthy.String())
isHealthy = false
assert.Equal(t, "unhealthy", isHealthy.String())
}
func TestUpdater(t *testing.T) {
updater := &updater{}
assert.Equal(t, nil, updater.Check())
updater.status = errors.New("unhealthy")
assert.Equal(t, "unhealthy", updater.Check().Error())
}
func TestHTTPStatusCodeHealthChecker(t *testing.T) {
handler := &test.RequestHandlerMapping{
Method: http.MethodGet,
Pattern: "/health",
Handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
},
}
server := test.NewServer(handler)
defer server.Close()
url := server.URL + "/health"
checker := HTTPStatusCodeHealthChecker(
http.MethodGet, url, map[string][]string{
"key": {"value"},
}, 5*time.Second, http.StatusOK)
assert.Equal(t, nil, checker.Check())
checker = HTTPStatusCodeHealthChecker(
http.MethodGet, url, nil, 5*time.Second, http.StatusUnauthorized)
assert.NotEqual(t, nil, checker.Check())
}
func TestPeriodicHealthChecker(t *testing.T) {
firstCheck := true
checkFunc := func() error {
time.Sleep(2 * time.Second)
if firstCheck {
firstCheck = false
return nil
}
return errors.New("unhealthy")
}
checker := PeriodicHealthChecker(health.CheckFunc(checkFunc), 1*time.Second)
assert.Equal(t, "unknown status", checker.Check().Error())
time.Sleep(3 * time.Second)
assert.Equal(t, nil, checker.Check())
time.Sleep(3 * time.Second)
assert.Equal(t, "unhealthy", checker.Check().Error())
}
func fakeHealthChecker(healthy bool) health.Checker {
return health.CheckFunc(func() error {
if healthy {
return nil
}
return errors.New("unhealthy")
})
}
func TestCheckHealth(t *testing.T) {
// component01: healthy, component02: healthy => status: healthy
healthCheckerRegistry = map[string]health.Checker{}
healthCheckerRegistry["component01"] = fakeHealthChecker(true)
healthCheckerRegistry["component02"] = fakeHealthChecker(true)
status := map[string]interface{}{}
err := handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/health",
}, &status)
require.Nil(t, err)
assert.Equal(t, "healthy", status["status"].(string))
// component01: healthy, component02: unhealthy => status: unhealthy
healthCheckerRegistry = map[string]health.Checker{}
healthCheckerRegistry["component01"] = fakeHealthChecker(true)
healthCheckerRegistry["component02"] = fakeHealthChecker(false)
status = map[string]interface{}{}
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/health",
}, &status)
require.Nil(t, err)
assert.Equal(t, "unhealthy", status["status"].(string))
}
func TestCoreHealthChecker(t *testing.T) {
checker := coreHealthChecker()
assert.Equal(t, nil, checker.Check())
}
func TestDatabaseHealthChecker(t *testing.T) {
checker := databaseHealthChecker()
time.Sleep(1 * time.Second)
assert.Equal(t, nil, checker.Check())
}
func TestRegisterHealthCheckers(t *testing.T) {
healthCheckerRegistry = map[string]health.Checker{}
registerHealthCheckers()
assert.NotNil(t, healthCheckerRegistry["core"])
}

View File

@ -22,6 +22,7 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/core"
rep_models "github.com/goharbor/harbor/src/replication/models"
@ -65,15 +66,36 @@ func (l *LabelAPI) Prepare() {
return
}
if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() ||
label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
l.label = label
}
}
func (l *LabelAPI) requireAccess(label *models.Label, action rbac.Action, subresources ...rbac.Resource) bool {
var hasPermission bool
switch label.Scope {
case common.LabelScopeGlobal:
hasPermission = l.SecurityCtx.IsSysAdmin()
case common.LabelScopeProject:
if len(subresources) == 0 {
subresources = append(subresources, rbac.ResourceLabel)
}
resource := rbac.NewProjectNamespace(label.ProjectID).Resource(subresources...)
hasPermission = l.SecurityCtx.Can(action, resource)
}
if !hasPermission {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
} else {
l.HandleForbidden(l.SecurityCtx.GetUsername())
}
return false
}
return true
}
// Post creates a label
func (l *LabelAPI) Post() {
label := &models.Label{}
@ -82,10 +104,6 @@ func (l *LabelAPI) Post() {
switch label.Scope {
case common.LabelScopeGlobal:
if !l.SecurityCtx.IsSysAdmin() {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
label.ProjectID = 0
case common.LabelScopeProject:
exist, err := l.ProjectMgr.Exists(label.ProjectID)
@ -98,10 +116,10 @@ func (l *LabelAPI) Post() {
l.HandleNotFound(fmt.Sprintf("project %d not found", label.ProjectID))
return
}
if !l.SecurityCtx.HasAllPerm(label.ProjectID) {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
}
if !l.requireAccess(label, rbac.ActionCreate) {
return
}
labels, err := dao.ListLabels(&models.LabelQuery{
@ -147,15 +165,8 @@ func (l *LabelAPI) Get() {
return
}
if label.Scope == common.LabelScopeProject {
if !l.SecurityCtx.HasReadPerm(label.ProjectID) {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
if !l.requireAccess(label, rbac.ActionRead) {
return
}
l.Data["json"] = label
@ -189,7 +200,8 @@ func (l *LabelAPI) List() {
return
}
if !l.SecurityCtx.HasReadPerm(projectID) {
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceLabel)
if !l.SecurityCtx.Can(rbac.ActionList, resource) {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
@ -221,6 +233,10 @@ func (l *LabelAPI) List() {
// Put updates the label
func (l *LabelAPI) Put() {
if !l.requireAccess(l.label, rbac.ActionUpdate) {
return
}
label := &models.Label{}
l.DecodeJSONReq(label)
@ -259,6 +275,10 @@ func (l *LabelAPI) Put() {
// Delete the label
func (l *LabelAPI) Delete() {
if !l.requireAccess(l.label, rbac.ActionDelete) {
return
}
id := l.label.ID
if err := dao.DeleteResourceLabelByLabel(id); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to delete resource label mappings of label %d: %v", id, err))
@ -272,11 +292,6 @@ func (l *LabelAPI) Delete() {
// ListResources lists the resources that the label is referenced by
func (l *LabelAPI) ListResources() {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 {
l.HandleBadRequest("invalid label ID")
@ -294,9 +309,7 @@ func (l *LabelAPI) ListResources() {
return
}
if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() ||
label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) {
l.HandleForbidden(l.SecurityCtx.GetUsername())
if !l.requireAccess(label, rbac.ActionList, rbac.ResourceLabelResource) {
return
}

View File

@ -22,23 +22,6 @@ func (lra *LabelResourceAPI) Prepare() {
lra.labelManager = &label.BaseManager{}
}
func (lra *LabelResourceAPI) checkPermissions(project string) bool {
if lra.Ctx.Request.Method == http.MethodPost ||
lra.Ctx.Request.Method == http.MethodDelete {
if lra.SecurityCtx.HasWritePerm(project) {
return true
}
}
if lra.Ctx.Request.Method == http.MethodGet {
if lra.SecurityCtx.HasReadPerm(project) {
return true
}
}
return false
}
func (lra *LabelResourceAPI) getLabelsOfResource(rType string, rIDOrName interface{}) {
labels, err := lra.labelManager.GetLabelsOfResource(rType, rIDOrName)
if err != nil {

View File

@ -22,6 +22,7 @@ import (
"strings"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr/metamgr"
)
@ -72,24 +73,6 @@ func (m *MetadataAPI) Prepare() {
m.project = project
switch m.Ctx.Request.Method {
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete:
if !(m.Ctx.Request.Method == http.MethodGet && project.IsPublic()) {
if !m.SecurityCtx.IsAuthenticated() {
m.HandleUnauthorized()
return
}
if !m.SecurityCtx.HasReadPerm(project.ProjectID) {
m.HandleForbidden(m.SecurityCtx.GetUsername())
return
}
}
default:
log.Debugf("%s method not allowed", m.Ctx.Request.Method)
m.RenderError(http.StatusMethodNotAllowed, "")
return
}
name := m.GetStringFromPath(":name")
if len(name) > 0 {
m.name = name
@ -105,8 +88,27 @@ func (m *MetadataAPI) Prepare() {
}
}
func (m *MetadataAPI) requireAccess(action rbac.Action) bool {
resource := rbac.NewProjectNamespace(m.project.ProjectID).Resource(rbac.ResourceMetadata)
if !m.SecurityCtx.Can(action, resource) {
if !m.SecurityCtx.IsAuthenticated() {
m.HandleUnauthorized()
} else {
m.HandleForbidden(m.SecurityCtx.GetUsername())
}
return false
}
return true
}
// Get ...
func (m *MetadataAPI) Get() {
if !m.requireAccess(rbac.ActionRead) {
return
}
var metas map[string]string
var err error
if len(m.name) > 0 {
@ -125,6 +127,10 @@ func (m *MetadataAPI) Get() {
// Post ...
func (m *MetadataAPI) Post() {
if !m.requireAccess(rbac.ActionCreate) {
return
}
var metas map[string]string
m.DecodeJSONReq(&metas)
@ -161,6 +167,10 @@ func (m *MetadataAPI) Post() {
// Put ...
func (m *MetadataAPI) Put() {
if !m.requireAccess(rbac.ActionUpdate) {
return
}
var metas map[string]string
m.DecodeJSONReq(&metas)
@ -188,6 +198,10 @@ func (m *MetadataAPI) Put() {
// Delete ...
func (m *MetadataAPI) Delete() {
if !m.requireAccess(rbac.ActionDelete) {
return
}
if err := m.metaMgr.Delete(m.project.ProjectID, m.name); err != nil {
m.HandleInternalServerError(fmt.Sprintf("failed to delete metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
return

View File

@ -71,6 +71,9 @@ type GCRep struct {
// Valid validates the gc request
func (gr *GCReq) Valid(v *validation.Validation) {
if gr.Schedule == nil {
return
}
switch gr.Schedule.Type {
case ScheduleDaily, ScheduleWeekly:
if gr.Schedule.Offtime < 0 || gr.Schedule.Offtime > 3600*24 {

View File

@ -14,7 +14,6 @@
package models
import (
"log"
"testing"
"github.com/stretchr/testify/assert"
@ -22,6 +21,9 @@ import (
"github.com/goharbor/harbor/src/common"
common_job "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"os"
"strings"
)
var adminServerTestConfig = map[string]interface{}{
@ -29,11 +31,11 @@ var adminServerTestConfig = map[string]interface{}{
}
func TestMain(m *testing.M) {
server, err := test.NewAdminserver(adminServerTestConfig)
if err != nil {
log.Fatalf("failed to create a mock admin server: %v", err)
}
defer server.Close()
test.InitDatabaseFromEnv()
config.Init()
config.Upload(adminServerTestConfig)
os.Exit(m.Run())
}
@ -126,5 +128,5 @@ func TestCronString(t *testing.T) {
Schedule: schedule,
}
cronStr := adminjob.CronString()
assert.Equal(t, cronStr, "{\"type\":\"Daily\",\"Weekday\":0,\"Offtime\":102}")
assert.True(t, strings.EqualFold(cronStr, "{\"type\":\"Daily\",\"Weekday\":0,\"Offtime\":102}"))
}

View File

@ -22,6 +22,7 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
errutil "github.com/goharbor/harbor/src/common/utils/error"
"github.com/goharbor/harbor/src/common/utils/log"
@ -77,6 +78,25 @@ func (p *ProjectAPI) Prepare() {
}
}
func (p *ProjectAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
if len(subresource) == 0 {
subresource = append(subresource, rbac.ResourceSelf)
}
resource := rbac.NewProjectNamespace(p.project.ProjectID).Resource(subresource...)
if !p.SecurityCtx.Can(action, resource) {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
} else {
p.HandleForbidden(p.SecurityCtx.GetUsername())
}
return false
}
return true
}
// Post ...
func (p *ProjectAPI) Post() {
if !p.SecurityCtx.IsAuthenticated() {
@ -187,16 +207,8 @@ func (p *ProjectAPI) Head() {
// Get ...
func (p *ProjectAPI) Get() {
if !p.project.IsPublic() {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
return
}
if !p.SecurityCtx.HasReadPerm(p.project.ProjectID) {
p.HandleForbidden(p.SecurityCtx.GetUsername())
return
}
if !p.requireAccess(rbac.ActionRead) {
return
}
p.populateProperties(p.project)
@ -207,13 +219,7 @@ func (p *ProjectAPI) Get() {
// Delete ...
func (p *ProjectAPI) Delete() {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
return
}
if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) {
p.HandleForbidden(p.SecurityCtx.GetUsername())
if !p.requireAccess(rbac.ActionDelete) {
return
}
@ -248,13 +254,7 @@ func (p *ProjectAPI) Delete() {
// Deletable ...
func (p *ProjectAPI) Deletable() {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
return
}
if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) {
p.HandleForbidden(p.SecurityCtx.GetUsername())
if !p.requireAccess(rbac.ActionDelete) {
return
}
@ -433,13 +433,7 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
// Put ...
func (p *ProjectAPI) Put() {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
return
}
if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) {
p.HandleForbidden(p.SecurityCtx.GetUsername())
if !p.requireAccess(rbac.ActionUpdate) {
return
}
@ -458,13 +452,7 @@ func (p *ProjectAPI) Put() {
// Logs ...
func (p *ProjectAPI) Logs() {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
return
}
if !p.SecurityCtx.HasReadPerm(p.project.ProjectID) {
p.HandleForbidden(p.SecurityCtx.GetUsername())
if !p.requireAccess(rbac.ActionList, rbac.ResourceLog) {
return
}

View File

@ -24,6 +24,7 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao/project"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/auth"
)
@ -73,12 +74,6 @@ func (pma *ProjectMemberAPI) Prepare() {
}
pma.project = project
if !(pma.Ctx.Input.IsGet() && pma.SecurityCtx.HasReadPerm(pid) ||
pma.SecurityCtx.HasAllPerm(pid)) {
pma.HandleForbidden(pma.SecurityCtx.GetUsername())
return
}
pmid, err := pma.GetInt64FromPath(":pmid")
if err != nil {
log.Warningf("Failed to get pmid from path, error %v", err)
@ -90,6 +85,22 @@ func (pma *ProjectMemberAPI) Prepare() {
pma.id = int(pmid)
}
func (pma *ProjectMemberAPI) requireAccess(action rbac.Action) bool {
resource := rbac.NewProjectNamespace(pma.project.ProjectID).Resource(rbac.ResourceMember)
if !pma.SecurityCtx.Can(action, resource) {
if !pma.SecurityCtx.IsAuthenticated() {
pma.HandleUnauthorized()
} else {
pma.HandleForbidden(pma.SecurityCtx.GetUsername())
}
return false
}
return true
}
// Get ...
func (pma *ProjectMemberAPI) Get() {
projectID := pma.project.ProjectID
@ -97,6 +108,9 @@ func (pma *ProjectMemberAPI) Get() {
queryMember.ProjectID = projectID
pma.Data["json"] = make([]models.Member, 0)
if pma.id == 0 {
if !pma.requireAccess(rbac.ActionList) {
return
}
entityname := pma.GetString("entityname")
memberList, err := project.SearchMemberByName(projectID, entityname)
if err != nil {
@ -119,6 +133,10 @@ func (pma *ProjectMemberAPI) Get() {
pma.HandleNotFound(fmt.Sprintf("The project member does not exit, pmid:%v", pma.id))
return
}
if !pma.requireAccess(rbac.ActionRead) {
return
}
pma.Data["json"] = memberList[0]
}
pma.ServeJSON()
@ -126,6 +144,9 @@ func (pma *ProjectMemberAPI) Get() {
// Post ... Add a project member
func (pma *ProjectMemberAPI) Post() {
if !pma.requireAccess(rbac.ActionCreate) {
return
}
projectID := pma.project.ProjectID
var request models.MemberReq
pma.DecodeJSONReq(&request)
@ -156,11 +177,14 @@ func (pma *ProjectMemberAPI) Post() {
// Put ... Update an exist project member
func (pma *ProjectMemberAPI) Put() {
if !pma.requireAccess(rbac.ActionUpdate) {
return
}
pid := pma.project.ProjectID
pmID := pma.id
var req models.Member
pma.DecodeJSONReq(&req)
if req.Role < 1 || req.Role > 3 {
if req.Role < 1 || req.Role > 4 {
pma.HandleBadRequest(fmt.Sprintf("Invalid role id %v", req.Role))
return
}
@ -173,6 +197,9 @@ func (pma *ProjectMemberAPI) Put() {
// Delete ...
func (pma *ProjectMemberAPI) Delete() {
if !pma.requireAccess(rbac.ActionDelete) {
return
}
pmid := pma.id
err := project.DeleteProjectMemberByID(pmid)
if err != nil {
@ -226,7 +253,7 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) {
return 0, ErrDuplicateProjectMember
}
if member.Role < 1 || member.Role > 3 {
if member.Role < 1 || member.Role > 4 {
// Return invalid role error
return 0, ErrInvalidRole
}

View File

@ -209,6 +209,18 @@ func TestProjectMemberAPI_PutAndDelete(t *testing.T) {
},
code: http.StatusOK,
},
// 200
{
request: &testingRequest{
method: http.MethodPut,
url: URL,
bodyJSON: &models.Member{
Role: 4,
},
credential: admin,
},
code: http.StatusOK,
},
// 400
{
request: &testingRequest{

View File

@ -24,6 +24,7 @@ import (
common_http "github.com/goharbor/harbor/src/common/http"
common_job "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
api_models "github.com/goharbor/harbor/src/core/api/models"
"github.com/goharbor/harbor/src/core/utils"
@ -80,7 +81,8 @@ func (ra *RepJobAPI) List() {
return
}
if !ra.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
return
}
@ -190,7 +192,8 @@ func (ra *RepJobAPI) GetLog() {
return
}
if !ra.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
return
}

View File

@ -22,6 +22,7 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
api_models "github.com/goharbor/harbor/src/core/api/models"
"github.com/goharbor/harbor/src/core/promgr"
@ -63,7 +64,8 @@ func (pa *RepPolicyAPI) Get() {
return
}
if !pa.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication)
if !pa.SecurityCtx.Can(rbac.ActionRead, resource) {
pa.HandleForbidden(pa.SecurityCtx.GetUsername())
return
}
@ -105,7 +107,8 @@ func (pa *RepPolicyAPI) List() {
if result != nil {
total = result.Total
for _, policy := range result.Policies {
if !pa.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication)
if !pa.SecurityCtx.Can(rbac.ActionRead, resource) {
continue
}
ply, err := convertFromRepPolicy(pa.ProjectMgr, *policy)

View File

@ -30,6 +30,7 @@ import (
"github.com/goharbor/harbor/src/common/dao"
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/clair"
"github.com/goharbor/harbor/src/common/utils/log"
@ -131,7 +132,8 @@ func (ra *RepositoryAPI) Get() {
return
}
if !ra.SecurityCtx.HasReadPerm(projectID) {
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
return
@ -247,7 +249,8 @@ func (ra *RepositoryAPI) Delete() {
return
}
if !ra.SecurityCtx.HasAllPerm(projectName) {
resource := rbac.NewProjectNamespace(project.ProjectID).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionDelete, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
return
}
@ -393,7 +396,8 @@ func (ra *RepositoryAPI) GetTag() {
return
}
project, _ := utils.ParseRepository(repository)
if !ra.SecurityCtx.HasReadPerm(project) {
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTag)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
return
@ -488,14 +492,16 @@ func (ra *RepositoryAPI) Retag() {
}
// Check whether user has read permission to source project
if !ra.SecurityCtx.HasReadPerm(srcImage.Project) {
srcResource := rbac.NewProjectNamespace(srcImage.Project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionPull, srcResource) {
log.Errorf("user has no read permission to project '%s'", srcImage.Project)
ra.HandleForbidden(fmt.Sprintf("%s has no read permission to project %s", ra.SecurityCtx.GetUsername(), srcImage.Project))
return
}
// Check whether user has write permission to target project
if !ra.SecurityCtx.HasWritePerm(project) {
destResource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionPush, destResource) {
log.Errorf("user has no write permission to project '%s'", project)
ra.HandleForbidden(fmt.Sprintf("%s has no write permission to project %s", ra.SecurityCtx.GetUsername(), project))
return
@ -533,7 +539,8 @@ func (ra *RepositoryAPI) GetTags() {
return
}
if !ra.SecurityCtx.HasReadPerm(projectName) {
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTag)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
return
@ -741,7 +748,8 @@ func (ra *RepositoryAPI) GetManifests() {
return
}
if !ra.SecurityCtx.HasReadPerm(projectName) {
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagManifest)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
return
@ -872,7 +880,8 @@ func (ra *RepositoryAPI) Put() {
}
project, _ := utils.ParseRepository(name)
if !ra.SecurityCtx.HasWritePerm(project) {
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionUpdate, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
return
}
@ -906,7 +915,8 @@ func (ra *RepositoryAPI) GetSignatures() {
return
}
if !ra.SecurityCtx.HasReadPerm(projectName) {
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
return
@ -949,7 +959,9 @@ func (ra *RepositoryAPI) ScanImage() {
ra.HandleUnauthorized()
return
}
if !ra.SecurityCtx.HasAllPerm(projectName) {
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
if !ra.SecurityCtx.Can(rbac.ActionCreate, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
return
}
@ -980,7 +992,9 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
return
}
project, _ := utils.ParseRepository(repository)
if !ra.SecurityCtx.HasReadPerm(project) {
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTagVulnerability)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
return

View File

@ -22,7 +22,7 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/rbac"
coreutils "github.com/goharbor/harbor/src/core/utils"
)
@ -45,12 +45,6 @@ func (r *RepositoryLabelAPI) Prepare() {
}
repository := r.GetString(":splat")
project, _ := utils.ParseRepository(repository)
if !r.checkPermissions(project) {
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
return
}
repo, err := dao.GetRepositoryByName(repository)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to get repository %s: %v", repository, err))
@ -77,25 +71,6 @@ func (r *RepositoryLabelAPI) Prepare() {
r.tag = tag
}
if r.Ctx.Request.Method == http.MethodPost {
p, err := r.ProjectMgr.Get(project)
if err != nil {
r.SendInternalServerError(err)
return
}
l := &models.Label{}
r.DecodeJSONReq(l)
label, ok := r.validate(l.ID, p.ProjectID)
if !ok {
return
}
r.label = label
return
}
if r.Ctx.Request.Method == http.MethodDelete {
labelID, err := r.GetInt64FromPath(":id")
if err != nil {
@ -112,13 +87,59 @@ func (r *RepositoryLabelAPI) Prepare() {
}
}
func (r *RepositoryLabelAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
if len(subresource) == 0 {
subresource = append(subresource, rbac.ResourceRepositoryLabel)
}
resource := rbac.NewProjectNamespace(r.repository.ProjectID).Resource(rbac.ResourceRepositoryLabel)
if !r.SecurityCtx.Can(action, resource) {
if !r.SecurityCtx.IsAuthenticated() {
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
} else {
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
}
return false
}
return true
}
func (r *RepositoryLabelAPI) isValidLabelReq() bool {
p, err := r.ProjectMgr.Get(r.repository.ProjectID)
if err != nil {
r.SendInternalServerError(err)
return false
}
l := &models.Label{}
r.DecodeJSONReq(l)
label, ok := r.validate(l.ID, p.ProjectID)
if !ok {
return false
}
r.label = label
return true
}
// GetOfImage returns labels of an image
func (r *RepositoryLabelAPI) GetOfImage() {
if !r.requireAccess(rbac.ActionList, rbac.ResourceRepositoryTagLabel) {
return
}
r.getLabelsOfResource(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag))
}
// AddToImage adds the label to an image
func (r *RepositoryLabelAPI) AddToImage() {
if !r.requireAccess(rbac.ActionCreate, rbac.ResourceRepositoryTagLabel) || !r.isValidLabelReq() {
return
}
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeImage,
@ -129,17 +150,29 @@ func (r *RepositoryLabelAPI) AddToImage() {
// RemoveFromImage removes the label from an image
func (r *RepositoryLabelAPI) RemoveFromImage() {
if !r.requireAccess(rbac.ActionDelete, rbac.ResourceRepositoryTagLabel) {
return
}
r.removeLabelFromResource(common.ResourceTypeImage,
fmt.Sprintf("%s:%s", r.repository.Name, r.tag), r.label.ID)
}
// GetOfRepository returns labels of a repository
func (r *RepositoryLabelAPI) GetOfRepository() {
if !r.requireAccess(rbac.ActionList) {
return
}
r.getLabelsOfResource(common.ResourceTypeRepository, r.repository.RepositoryID)
}
// AddToRepository adds the label to a repository
func (r *RepositoryLabelAPI) AddToRepository() {
if !r.requireAccess(rbac.ActionCreate) || !r.isValidLabelReq() {
return
}
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeRepository,
@ -150,6 +183,10 @@ func (r *RepositoryLabelAPI) AddToRepository() {
// RemoveFromRepository removes the label from a repository
func (r *RepositoryLabelAPI) RemoveFromRepository() {
if !r.requireAccess(rbac.ActionDelete) {
return
}
r.removeLabelFromResource(common.ResourceTypeRepository, r.repository.RepositoryID, r.label.ID)
}

238
src/core/api/robot.go Normal file
View File

@ -0,0 +1,238 @@
// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"strconv"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/token"
)
// RobotAPI ...
type RobotAPI struct {
BaseController
project *models.Project
robot *models.Robot
}
// Prepare ...
func (r *RobotAPI) Prepare() {
r.BaseController.Prepare()
method := r.Ctx.Request.Method
if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized()
return
}
pid, err := r.GetInt64FromPath(":pid")
if err != nil || pid <= 0 {
var errMsg string
if err != nil {
errMsg = "failed to get project ID " + err.Error()
} else {
errMsg = "invalid project ID: " + fmt.Sprintf("%d", pid)
}
r.HandleBadRequest(errMsg)
return
}
project, err := r.ProjectMgr.Get(pid)
if err != nil {
r.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err)
return
}
if project == nil {
r.HandleNotFound(fmt.Sprintf("project %d not found", pid))
return
}
r.project = project
if method == http.MethodPut || method == http.MethodDelete {
id, err := r.GetInt64FromPath(":id")
if err != nil || id <= 0 {
r.HandleBadRequest("invalid robot ID")
return
}
robot, err := dao.GetRobotByID(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err))
return
}
if robot == nil {
r.HandleNotFound(fmt.Sprintf("robot %d not found", id))
return
}
r.robot = robot
}
}
func (r *RobotAPI) requireAccess(action rbac.Action) bool {
resource := rbac.NewProjectNamespace(r.project.ProjectID).Resource(rbac.ResourceRobot)
if !r.SecurityCtx.Can(action, resource) {
r.HandleForbidden(r.SecurityCtx.GetUsername())
return false
}
return true
}
// Post ...
func (r *RobotAPI) Post() {
if !r.requireAccess(rbac.ActionCreate) {
return
}
var robotReq models.RobotReq
r.DecodeJSONReq(&robotReq)
createdName := common.RobotPrefix + robotReq.Name
// first to add a robot account, and get its id.
robot := models.Robot{
Name: createdName,
Description: robotReq.Description,
ProjectID: r.project.ProjectID,
}
id, err := dao.AddRobot(&robot)
if err != nil {
if err == dao.ErrDupRows {
r.HandleConflict()
return
}
r.HandleInternalServerError(fmt.Sprintf("failed to create robot account: %v", err))
return
}
// generate the token, and return it with response data.
// token is not stored in the database.
jwtToken, err := token.New(id, r.project.ProjectID, robotReq.Access)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err))
err := dao.DeleteRobot(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err))
}
return
}
rawTk, err := jwtToken.Raw()
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to sign token for robot account, %v", err))
err := dao.DeleteRobot(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err))
}
return
}
robotRep := models.RobotRep{
Name: robot.Name,
Token: rawTk,
}
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
r.Data["json"] = robotRep
r.ServeJSON()
}
// List list all the robots of a project
func (r *RobotAPI) List() {
if !r.requireAccess(rbac.ActionList) {
return
}
query := models.RobotQuery{
ProjectID: r.project.ProjectID,
}
count, err := dao.CountRobot(&query)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to list robots on project: %d, %v", r.project.ProjectID, err))
return
}
query.Page, query.Size = r.GetPaginationParams()
robots, err := dao.ListRobots(&query)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robots %v", err))
return
}
r.SetPaginationHeader(count, query.Page, query.Size)
r.Data["json"] = robots
r.ServeJSON()
}
// Get get robot by id
func (r *RobotAPI) Get() {
if !r.requireAccess(rbac.ActionRead) {
return
}
id, err := r.GetInt64FromPath(":id")
if err != nil || id <= 0 {
r.HandleBadRequest(fmt.Sprintf("invalid robot ID: %s", r.GetStringFromPath(":id")))
return
}
robot, err := dao.GetRobotByID(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err))
return
}
if robot == nil {
r.HandleNotFound(fmt.Sprintf("robot %d not found", id))
return
}
r.Data["json"] = robot
r.ServeJSON()
}
// Put disable or enable a robot account
func (r *RobotAPI) Put() {
if !r.requireAccess(rbac.ActionUpdate) {
return
}
var robotReq models.RobotReq
r.DecodeJSONReqAndValidate(&robotReq)
r.robot.Disabled = robotReq.Disabled
if err := dao.UpdateRobot(r.robot); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to update robot %d: %v", r.robot.ID, err))
return
}
}
// Delete delete robot by id
func (r *RobotAPI) Delete() {
if !r.requireAccess(rbac.ActionDelete) {
return
}
if err := dao.DeleteRobot(r.robot.ID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete robot %d: %v", r.robot.ID, err))
return
}
}

324
src/core/api/robot_test.go Normal file
View File

@ -0,0 +1,324 @@
// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"net/http"
"testing"
)
var (
robotPath = "/api/projects/1/robots"
robotID int64
)
func TestRobotAPIPost(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.RobotReq{},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 201
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.RobotReq{
Name: "test",
Description: "test desc",
Access: policies,
},
credential: projAdmin4Robot,
},
code: http.StatusCreated,
},
// 403 -- developer
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.RobotReq{
Name: "test2",
Description: "test2 desc",
},
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 409
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.RobotReq{
Name: "test",
Description: "test desc",
Access: policies,
},
credential: projAdmin4Robot,
},
code: http.StatusConflict,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIGet(t *testing.T) {
cases := []*codeCheckingCase{
// 400
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 0),
},
code: http.StatusUnauthorized,
},
// 404
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 1000),
credential: projDeveloper,
},
code: http.StatusNotFound,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projDeveloper,
},
code: http.StatusOK,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIList(t *testing.T) {
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodGet,
url: robotPath,
},
code: http.StatusUnauthorized,
},
// 400
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/0/robots",
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: robotPath,
credential: projDeveloper,
},
code: http.StatusOK,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: robotPath,
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIPut(t *testing.T) {
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
},
code: http.StatusUnauthorized,
},
// 400
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 0),
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
// 404
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 10000),
credential: projAdmin4Robot,
},
code: http.StatusNotFound,
},
// 403 non-member user
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 200
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
bodyJSON: &models.Robot{
Disabled: true,
},
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIDelete(t *testing.T) {
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
},
code: http.StatusUnauthorized,
},
// 400
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 0),
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
// 404
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 10000),
credential: projAdmin4Robot,
},
code: http.StatusNotFound,
},
// 403 non-member user
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 200
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -17,6 +17,7 @@ package api
import (
"github.com/goharbor/harbor/src/common/dao"
common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/utils"
@ -54,7 +55,9 @@ func (sj *ScanJobAPI) Prepare() {
sj.CustomAbort(http.StatusInternalServerError, "Failed to get Job data")
}
projectName := strings.SplitN(data.Repository, "/", 2)[0]
if !sj.SecurityCtx.HasReadPerm(projectName) {
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
if !sj.SecurityCtx.Can(rbac.ActionRead, resource) {
log.Errorf("User does not have read permission for project: %s", projectName)
sj.HandleForbidden(sj.SecurityCtx.GetUsername())
}

View File

@ -16,14 +16,12 @@ package api
import (
"fmt"
"net/http"
"os"
"testing"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/test"
"k8s.io/helm/cmd/helm/search"
"github.com/goharbor/harbor/src/common/dao"
@ -182,43 +180,30 @@ func TestSearch(t *testing.T) {
_, exist = repositories["search-2/hello-world"]
assert.True(t, exist)
currentAdminServerURL, ok := os.LookupEnv("ADMINSERVER_URL")
if ok {
chartSettings := map[string]interface{}{
common.WithChartMuseum: true,
}
adminServer, err := test.NewAdminserver(chartSettings)
if err != nil {
t.Fatal(nil)
}
defer adminServer.Close()
if err := config.InitByURL(adminServer.URL); err != nil {
t.Fatal(err)
}
defer func() {
// reset config
if err := config.InitByURL(currentAdminServerURL); err != nil {
t.Error(err)
}
}()
// Search chart
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/search",
queryStruct: struct {
Keyword string `url:"q"`
}{
Keyword: "harbor",
},
credential: sysAdmin,
}, result)
require.Nil(t, err)
require.Equal(t, 1, len(result.Chart))
require.Equal(t, "library/harbor", result.Chart[0].Name)
// Restore chart search handler
searchHandler = nil
chartSettings := map[string]interface{}{
common.WithChartMuseum: true,
}
config.InitWithSettings(chartSettings)
defer func() {
// reset config
config.Init()
}()
// Search chart
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/search",
queryStruct: struct {
Keyword string `url:"q"`
}{
Keyword: "harbor",
},
credential: sysAdmin,
}, result)
require.Nil(t, err)
require.Equal(t, 1, len(result.Chart))
require.Equal(t, "library/harbor", result.Chart[0].Name)
// Restore chart search handler
searchHandler = nil
}

View File

@ -16,9 +16,10 @@ package api
import (
"encoding/json"
"fmt"
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert"
"testing"
)
func TestGetVolumeInfo(t *testing.T) {
@ -53,6 +54,9 @@ func TestGetVolumeInfo(t *testing.T) {
}
func TestGetGeneralInfo(t *testing.T) {
config.Upload(map[string]interface{}{
common.ReadOnly: false,
})
apiTest := newHarborAPI()
code, body, err := apiTest.GetGeneralInfo()
assert := assert.New(t)

View File

@ -24,6 +24,8 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
@ -339,6 +341,57 @@ func (ua *UserAPI) ToggleUserAdminRole() {
}
}
// ListUserPermissions handles GET to /api/users/{}/permissions
func (ua *UserAPI) ListUserPermissions() {
if ua.userID != ua.currentUserID {
log.Warningf("Current user, id: %d can not view other user's permissions", ua.currentUserID)
ua.RenderError(http.StatusForbidden, "User does not have permission")
return
}
relative := ua.Ctx.Input.Query("relative") == "true"
scope := rbac.Resource(ua.Ctx.Input.Query("scope"))
policies := []*rbac.Policy{}
namespace, err := scope.GetNamespace()
if err == nil {
switch namespace.Kind() {
case "project":
for _, policy := range project.GetAllPolicies(namespace) {
if ua.SecurityCtx.Can(policy.Action, policy.Resource) {
policies = append(policies, policy)
}
}
}
}
results := []map[string]string{}
for _, policy := range policies {
var resource rbac.Resource
// for resource `/project/1/repository` if `relative` is `true` then the resource in response will be `repository`
if relative {
relativeResource, err := policy.Resource.RelativeTo(scope)
if err != nil {
continue
}
resource = relativeResource
} else {
resource = policy.Resource
}
results = append(results, map[string]string{
"resource": resource.String(),
"action": policy.Action.String(),
})
}
ua.Data["json"] = results
ua.ServeJSON()
return
}
// modifiable returns whether the modify is allowed based on current auth mode and context
func (ua *UserAPI) modifiable() bool {
if ua.AuthMode == common.DBAuth {

View File

@ -27,6 +27,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/astaxie/beego"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/core/config"
)
var testUser0002ID, testUser0003ID int
@ -39,7 +41,9 @@ func TestUsersPost(t *testing.T) {
assert := assert.New(t)
apiTest := newHarborAPI()
config.Upload(map[string]interface{}{
common.AUTHMode: "db_auth",
})
// case 1: register a new user without admin auth, expect 400, because self registration is on
fmt.Println("Register user without admin auth")
code, err := apiTest.UsersPost(testUser0002)
@ -572,3 +576,28 @@ func TestModifiable(t *testing.T) {
}
assert.True(ua4.modifiable())
}
func TestUsersCurrentPermissions(t *testing.T) {
fmt.Println("Testing Get Users Current Permissions")
assert := assert.New(t)
apiTest := newHarborAPI()
httpStatusCode, permissions, err := apiTest.UsersGetPermissions("current", "/project/library", *projAdmin)
assert.Nil(err)
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.NotEmpty(permissions, "permissions should not be empty")
httpStatusCode, permissions, err = apiTest.UsersGetPermissions("current", "/unsupport-scope", *projAdmin)
assert.Nil(err)
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Empty(permissions, "permissions should be empty")
httpStatusCode, _, err = apiTest.UsersGetPermissions(projAdminID, "/project/library", *projAdmin)
assert.Nil(err)
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
httpStatusCode, _, err = apiTest.UsersGetPermissions(projDeveloperID, "/project/library", *projAdmin)
assert.Nil(err)
assert.Equal(int(403), httpStatusCode, "httpStatusCode should be 403")
}

View File

@ -17,34 +17,12 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
)
var l = NewUserLock(2 * time.Second)
var adminServerLdapTestConfig = map[string]interface{}{
common.ExtEndpoint: "host01.com",
common.AUTHMode: "ldap_auth",
common.DatabaseType: "postgresql",
common.PostGreSQLHOST: "127.0.0.1",
common.PostGreSQLPort: 5432,
common.PostGreSQLUsername: "postgres",
common.PostGreSQLPassword: "root123",
common.PostGreSQLDatabase: "registry",
common.LDAPURL: "ldap://127.0.0.1",
common.LDAPSearchDN: "cn=admin,dc=example,dc=com",
common.LDAPSearchPwd: "admin",
common.LDAPBaseDN: "dc=example,dc=com",
common.LDAPUID: "uid",
common.LDAPFilter: "",
common.LDAPScope: 3,
common.LDAPTimeout: 30,
common.CfgExpiration: 5,
common.AdminInitialPassword: "password",
}
func TestLock(t *testing.T) {
t.Log("Locking john")
l.Lock("john")

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