mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-18 21:01:27 +01:00
Merge remote-tracking branch 'upstream/master' into 190215_syncmaster
This commit is contained in:
commit
4ceaf3e6c9
@ -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:
|
||||
|
2
Makefile
2
Makefile
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
@ -3136,6 +3179,162 @@ 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: '#/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 +4369,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 +4379,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 +4713,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
|
||||
|
@ -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,
|
||||
|
1
make/migrations/postgresql/0005_add_master_role.up.sql
Normal file
1
make/migrations/postgresql/0005_add_master_role.up.sql
Normal file
@ -0,0 +1 @@
|
||||
INSERT INTO role (role_code, name) VALUES ('DRWS', 'master');
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
@ -115,6 +117,11 @@ 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$"
|
||||
)
|
||||
|
||||
// Shared variable, not allowed to modify
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
59
src/common/rbac/casbin_test.go
Normal file
59
src/common/rbac/casbin_test.go
Normal 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
56
src/common/rbac/const.go
Normal 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
|
||||
)
|
61
src/common/rbac/namespace.go
Normal file
61
src/common/rbac/namespace.go
Normal 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}
|
||||
}
|
45
src/common/rbac/namespace_test.go
Normal file
45
src/common/rbac/namespace_test.go
Normal 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
50
src/common/rbac/parser.go
Normal 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
|
||||
}
|
39
src/common/rbac/parser_test.go
Normal file
39
src/common/rbac/parser_test.go
Normal 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))
|
||||
}
|
166
src/common/rbac/project/util.go
Normal file
166
src/common/rbac/project/util.go
Normal 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
|
||||
}
|
83
src/common/rbac/project/visitor.go
Normal file
83
src/common/rbac/project/visitor.go
Normal 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,
|
||||
}
|
||||
}
|
301
src/common/rbac/project/visitor_role.go
Normal file
301
src/common/rbac/project/visitor_role.go
Normal 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
|
||||
}
|
44
src/common/rbac/project/visitor_role_test.go
Normal file
44
src/common/rbac/project/visitor_role_test.go
Normal 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))
|
||||
}
|
93
src/common/rbac/project/visitor_test.go
Normal file
93
src/common/rbac/project/visitor_test.go
Normal 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))
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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":
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"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/core/promgr"
|
||||
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
|
||||
@ -210,66 +211,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 +293,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) {
|
||||
|
88
src/common/security/robot/context.go
Normal file
88
src/common/security/robot/context.go
Normal 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
|
||||
}
|
200
src/common/security/robot/context_test.go
Normal file
200
src/common/security/robot/context_test.go
Normal 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)
|
||||
}
|
42
src/common/security/robot/robot.go
Normal file
42
src/common/security/robot/robot.go
Normal 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,
|
||||
}
|
||||
}
|
27
src/common/security/robot/robot_test.go
Normal file
27
src/common/security/robot/robot_test.go
Normal 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())
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
29
src/common/token/claims.go
Normal file
29
src/common/token/claims.go
Normal 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
|
||||
}
|
68
src/common/token/claims_test.go
Normal file
68
src/common/token/claims_test.go
Normal 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())
|
||||
}
|
85
src/common/token/htoken.go
Normal file
85
src/common/token/htoken.go
Normal 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
|
||||
}
|
77
src/common/token/htoken_test.go
Normal file
77
src/common/token/htoken_test.go
Normal 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())
|
||||
}
|
83
src/common/token/options.go
Normal file
83
src/common/token/options.go
Normal 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))
|
||||
}
|
||||
}
|
23
src/common/token/options_test.go
Normal file
23
src/common/token/options_test.go
Normal 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)
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -34,6 +34,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
||||
|
||||
// "strconv"
|
||||
// "strings"
|
||||
|
||||
@ -96,12 +97,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")
|
||||
@ -152,6 +155,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")
|
||||
@ -992,6 +998,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)
|
||||
|
323
src/core/api/health.go
Normal file
323
src/core/api/health.go
Normal 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
134
src/core/api/health_test.go
Normal 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"])
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
238
src/core/api/robot.go
Normal 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
324
src/core/api/robot_test.go
Normal 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...)
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -572,3 +572,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")
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ func Register(name string, h AuthenticateHelper) {
|
||||
return
|
||||
}
|
||||
registry[name] = h
|
||||
log.Debugf("Registered authencation helper for auth mode: %s", name)
|
||||
log.Debugf("Registered authentication helper for auth mode: %s", name)
|
||||
}
|
||||
|
||||
// Login authenticates user credentials based on setting.
|
||||
|
143
src/core/auth/authproxy/auth.go
Normal file
143
src/core/auth/authproxy/auth.go
Normal file
@ -0,0 +1,143 @@
|
||||
// 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 authproxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Auth implements HTTP authenticator the required attributes.
|
||||
// The attribute Endpoint is the HTTP endpoint to which the POST request should be issued for authentication
|
||||
type Auth struct {
|
||||
auth.DefaultAuthenticateHelper
|
||||
sync.Mutex
|
||||
Endpoint string
|
||||
SkipCertVerify bool
|
||||
AlwaysOnboard bool
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Authenticate issues http POST request to Endpoint if it returns 200 the authentication is considered success.
|
||||
func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
a.ensure()
|
||||
req, err := http.NewRequest(http.MethodPost, a.Endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request, error: %v", err)
|
||||
}
|
||||
req.SetBasicAuth(m.Principal, m.Password)
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return &models.User{Username: m.Principal}, nil
|
||||
} else if resp.StatusCode == http.StatusUnauthorized {
|
||||
return nil, auth.ErrAuth{}
|
||||
} else {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Warningf("Failed to read response body, error: %v", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to authenticate, status code: %d, text: %s", resp.StatusCode, string(data))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// OnBoardUser delegates to dao pkg to insert/update data in DB.
|
||||
func (a *Auth) OnBoardUser(u *models.User) error {
|
||||
return dao.OnBoardUser(u)
|
||||
}
|
||||
|
||||
// PostAuthenticate generates the user model and on board the user.
|
||||
func (a *Auth) PostAuthenticate(u *models.User) error {
|
||||
if res, _ := dao.GetUser(*u); res != nil {
|
||||
return nil
|
||||
}
|
||||
if err := a.fillInModel(u); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.OnBoardUser(u)
|
||||
}
|
||||
|
||||
// SearchUser - TODO: Remove this workaround when #6767 is fixed.
|
||||
// When the flag is set it always return the default model without searching
|
||||
func (a *Auth) SearchUser(username string) (*models.User, error) {
|
||||
a.ensure()
|
||||
var queryCondition = models.User{
|
||||
Username: username,
|
||||
}
|
||||
u, err := dao.GetUser(queryCondition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.AlwaysOnboard && u == nil {
|
||||
u = &models.User{Username: username}
|
||||
if err := a.fillInModel(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (a *Auth) fillInModel(u *models.User) error {
|
||||
if strings.TrimSpace(u.Username) == "" {
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
}
|
||||
u.Realname = u.Username
|
||||
u.Password = "1234567ab"
|
||||
u.Comment = "By Authproxy"
|
||||
if strings.Contains(u.Username, "@") {
|
||||
u.Email = u.Username
|
||||
} else {
|
||||
u.Email = fmt.Sprintf("%s@placeholder.com", u.Username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auth) ensure() {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
if a.Endpoint == "" {
|
||||
a.Endpoint = os.Getenv("AUTHPROXY_ENDPOINT")
|
||||
a.SkipCertVerify = strings.EqualFold(os.Getenv("AUTHPROXY_SKIP_CERT_VERIFY"), "true")
|
||||
a.AlwaysOnboard = strings.EqualFold(os.Getenv("AUTHPROXY_ALWAYS_ONBOARD"), "true")
|
||||
}
|
||||
if a.client == nil {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: a.SkipCertVerify,
|
||||
},
|
||||
}
|
||||
a.client = &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.Register(common.HTTPAuth, &Auth{})
|
||||
}
|
144
src/core/auth/authproxy/auth_test.go
Normal file
144
src/core/auth/authproxy/auth_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
// 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 authproxy
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/auth/authproxy/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var mockSvr *httptest.Server
|
||||
var a *Auth
|
||||
var pwd = "1234567ab"
|
||||
var cmt = "By Authproxy"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"})
|
||||
defer mockSvr.Close()
|
||||
a = &Auth{
|
||||
Endpoint: mockSvr.URL + "/test/login",
|
||||
SkipCertVerify: true,
|
||||
}
|
||||
rc := m.Run()
|
||||
if rc != 0 {
|
||||
os.Exit(rc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_Authenticate(t *testing.T) {
|
||||
t.Log("auth endpoint: ", a.Endpoint)
|
||||
type output struct {
|
||||
user models.User
|
||||
err error
|
||||
}
|
||||
type tc struct {
|
||||
input models.AuthModel
|
||||
expect output
|
||||
}
|
||||
suite := []tc{
|
||||
{
|
||||
input: models.AuthModel{
|
||||
Principal: "jt", Password: "pp"},
|
||||
expect: output{
|
||||
user: models.User{
|
||||
Username: "jt",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: models.AuthModel{
|
||||
Principal: "Admin@vsphere.local",
|
||||
Password: "Admin!23",
|
||||
},
|
||||
expect: output{
|
||||
user: models.User{
|
||||
Username: "Admin@vsphere.local",
|
||||
// Email: "Admin@placeholder.com",
|
||||
// Password: pwd,
|
||||
// Comment: fmt.Sprintf(cmtTmpl, path.Join(mockSvr.URL, "/test/login")),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: models.AuthModel{
|
||||
Principal: "jt",
|
||||
Password: "ppp",
|
||||
},
|
||||
expect: output{
|
||||
err: auth.ErrAuth{},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert := assert.New(t)
|
||||
for _, c := range suite {
|
||||
r, e := a.Authenticate(c.input)
|
||||
if c.expect.err == nil {
|
||||
assert.Nil(e)
|
||||
assert.Equal(c.expect.user, *r)
|
||||
} else {
|
||||
assert.Nil(r)
|
||||
assert.NotNil(e)
|
||||
if _, ok := e.(auth.ErrAuth); ok {
|
||||
assert.IsType(auth.ErrAuth{}, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: Enable this case after adminserver refactor is merged.
|
||||
func TestAuth_PostAuthenticate(t *testing.T) {
|
||||
type tc struct {
|
||||
input *models.User
|
||||
expect models.User
|
||||
}
|
||||
suite := []tc{
|
||||
{
|
||||
input: &models.User{
|
||||
Username: "jt",
|
||||
},
|
||||
expect: models.User{
|
||||
Username: "jt",
|
||||
Email: "jt@placeholder.com",
|
||||
Realname: "jt",
|
||||
Password: pwd,
|
||||
Comment: fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"),
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &models.User{
|
||||
Username: "Admin@vsphere.local",
|
||||
},
|
||||
expect: models.User{
|
||||
Username: "Admin@vsphere.local",
|
||||
Email: "jt@placeholder.com",
|
||||
Realname: "Admin@vsphere.local",
|
||||
Password: pwd,
|
||||
Comment: fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range suite {
|
||||
a.PostAuthenticate(c.input)
|
||||
assert.Equal(t, c.expect, *c.input)
|
||||
}
|
||||
}
|
||||
*/
|
49
src/core/auth/authproxy/test/server.go
Normal file
49
src/core/auth/authproxy/test/server.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
type authHandler struct {
|
||||
m map[string]string
|
||||
}
|
||||
|
||||
// ServeHTTP handles HTTP requests
|
||||
func (ah *authHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "", http.StatusMethodNotAllowed)
|
||||
}
|
||||
if u, p, ok := req.BasicAuth(); !ok {
|
||||
// Simulate a service error
|
||||
http.Error(rw, "", http.StatusInternalServerError)
|
||||
} else if pass, ok := ah.m[u]; !ok || pass != p {
|
||||
http.Error(rw, "", http.StatusUnauthorized)
|
||||
} else {
|
||||
_, e := rw.Write([]byte(`{"session_id": "hgx59wuWI3b0jcbtidv5mU1YCp-DOQ9NKR1iYKACdKCvbVn7"}`))
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewMockServer creates the mock server for testing
|
||||
func NewMockServer(creds map[string]string) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/test/login", &authHandler{m: creds})
|
||||
return httptest.NewTLSServer(mux)
|
||||
}
|
@ -63,7 +63,7 @@ func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
func (u *Auth) OnBoardUser(user *models.User) error {
|
||||
user.Username = strings.TrimSpace(user.Username)
|
||||
if len(user.Username) == 0 {
|
||||
return fmt.Errorf("The Username is empty")
|
||||
return fmt.Errorf("the Username is empty")
|
||||
}
|
||||
if len(user.Password) == 0 {
|
||||
user.Password = "1234567ab"
|
||||
|
@ -572,3 +572,36 @@ func GetChartMuseumEndpoint() (string, error) {
|
||||
|
||||
return chartEndpoint, nil
|
||||
}
|
||||
|
||||
// GetRedisOfRegURL returns the URL of Redis used by registry
|
||||
func GetRedisOfRegURL() string {
|
||||
return os.Getenv("_REDIS_URL_REG")
|
||||
}
|
||||
|
||||
// GetPortalURL returns the URL of portal
|
||||
func GetPortalURL() string {
|
||||
url := os.Getenv("PORTAL_URL")
|
||||
if len(url) == 0 {
|
||||
return common.DefaultPortalURL
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// GetRegistryCtlURL returns the URL of registryctl
|
||||
func GetRegistryCtlURL() string {
|
||||
url := os.Getenv("REGISTRYCTL_URL")
|
||||
if len(url) == 0 {
|
||||
return common.DefaultRegistryCtlURL
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// GetClairHealthCheckServerURL returns the URL of
|
||||
// the health check server of Clair
|
||||
func GetClairHealthCheckServerURL() string {
|
||||
url := os.Getenv("CLAIR_HEALTH_CHECK_SERVER_URL")
|
||||
if len(url) == 0 {
|
||||
return common.DefaultClairHealthCheckServerURL
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
@ -53,6 +53,11 @@ func TestConfig(t *testing.T) {
|
||||
if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil {
|
||||
t.Fatalf("failed to set env %s: %v", "KEY_PATH", err)
|
||||
}
|
||||
oriKeyPath := os.Getenv("TOKEN_PRIVATE_KEY_PATH")
|
||||
if err := os.Setenv("TOKEN_PRIVATE_KEY_PATH", ""); err != nil {
|
||||
t.Fatalf("failed to set env %s: %v", "TOKEN_PRIVATE_KEY_PATH", err)
|
||||
}
|
||||
defer os.Setenv("TOKEN_PRIVATE_KEY_PATH", oriKeyPath)
|
||||
|
||||
if err := Init(); err != nil {
|
||||
t.Fatalf("failed to initialize configurations: %v", err)
|
||||
|
@ -22,18 +22,23 @@ import (
|
||||
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
secstore "github.com/goharbor/harbor/src/common/secret"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
admr "github.com/goharbor/harbor/src/common/security/admiral"
|
||||
"github.com/goharbor/harbor/src/common/security/admiral/authcontext"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
|
||||
"github.com/goharbor/harbor/src/common/security/secret"
|
||||
"github.com/goharbor/harbor/src/common/token"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/admiral"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ContextValueKey for content value
|
||||
@ -95,6 +100,7 @@ func Init() {
|
||||
// standalone
|
||||
reqCtxModifiers = []ReqCtxModifier{
|
||||
&secretReqCtxModifier{config.SecretStore},
|
||||
&robotAuthReqCtxModifier{},
|
||||
&basicAuthReqCtxModifier{},
|
||||
&sessionReqCtxModifier{},
|
||||
&unauthorizedReqCtxModifier{}}
|
||||
@ -147,6 +153,47 @@ func (s *secretReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type robotAuthReqCtxModifier struct{}
|
||||
|
||||
func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
robotName, robotTk, ok := ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(robotName, common.RobotPrefix) {
|
||||
return false
|
||||
}
|
||||
rClaims := &token.RobotClaims{}
|
||||
htk, err := token.ParseWithClaims(robotTk, rClaims)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt robot token, %v", err)
|
||||
return false
|
||||
}
|
||||
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
|
||||
robot, err := dao.GetRobotByID(htk.Claims.(*token.RobotClaims).TokenID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get robot %s: %v", robotName, err)
|
||||
return false
|
||||
}
|
||||
if robot == nil {
|
||||
log.Error("the token provided doesn't exist.")
|
||||
return false
|
||||
}
|
||||
if robotName != robot.Name {
|
||||
log.Errorf("failed to authenticate : %v", robotName)
|
||||
return false
|
||||
}
|
||||
if robot.Disabled {
|
||||
log.Errorf("the robot account %s is disabled", robot.Name)
|
||||
return false
|
||||
}
|
||||
log.Debug("creating robot account security context...")
|
||||
pm := config.GlobalProjectMgr
|
||||
securCtx := robotCtx.NewSecurityContext(robot, pm, htk.Claims.(*token.RobotClaims).Access)
|
||||
setSecurCtxAndPM(ctx.Request, securCtx, pm)
|
||||
return true
|
||||
}
|
||||
|
||||
type basicAuthReqCtxModifier struct{}
|
||||
|
||||
func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
|
@ -122,6 +122,23 @@ func TestSecretReqCtxModifier(t *testing.T) {
|
||||
assert.NotNil(t, projectManager(ctx))
|
||||
}
|
||||
|
||||
func TestRobotReqCtxModifier(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
req.SetBasicAuth("robot$test1", "Harbor12345")
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to crate context: %v", err)
|
||||
}
|
||||
|
||||
modifier := &robotAuthReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
assert.False(t, modified)
|
||||
}
|
||||
|
||||
func TestBasicAuthReqCtxModifier(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/api"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/authproxy"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/db"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/uaa"
|
||||
|
@ -46,6 +46,7 @@ func initRouters() {
|
||||
beego.Router("/api/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
|
||||
beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/users/:id/permissions", &api.UserAPI{}, "get:ListUserPermissions")
|
||||
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
|
||||
beego.Router("/api/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{})
|
||||
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
|
||||
@ -56,6 +57,7 @@ func initRouters() {
|
||||
}
|
||||
|
||||
// API
|
||||
beego.Router("/api/health", &api.HealthAPI{}, "get:CheckHealth")
|
||||
beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping")
|
||||
beego.Router("/api/search", &api.SearchAPI{})
|
||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post")
|
||||
@ -64,6 +66,10 @@ func initRouters() {
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &api.MetadataAPI{}, "put:Put;delete:Delete")
|
||||
|
||||
beego.Router("/api/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List")
|
||||
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &api.RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
||||
|
||||
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
|
||||
beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
|
||||
beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put")
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/docker/distribution/registry/auth/token"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
@ -158,24 +159,25 @@ func (rep repositoryFilter) filter(ctx security.Context, pm promgr.ProjectManage
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
project := img.namespace
|
||||
projectName := img.namespace
|
||||
permission := ""
|
||||
|
||||
exist, err := pm.Exists(project)
|
||||
exist, err := pm.Exists(projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
log.Debugf("project %s does not exist, set empty permission", project)
|
||||
log.Debugf("project %s does not exist, set empty permission", projectName)
|
||||
a.Actions = []string{}
|
||||
return nil
|
||||
}
|
||||
|
||||
if ctx.HasAllPerm(project) {
|
||||
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository)
|
||||
if ctx.Can(rbac.ActionPush, resource) && ctx.Can(rbac.ActionPull, resource) {
|
||||
permission = "RWM"
|
||||
} else if ctx.HasWritePerm(project) {
|
||||
} else if ctx.Can(rbac.ActionPush, resource) {
|
||||
permission = "RW"
|
||||
} else if ctx.HasReadPerm(project) {
|
||||
} else if ctx.Can(rbac.ActionPull, resource) {
|
||||
permission = "R"
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
@ -251,13 +252,7 @@ func (f *fakeSecurityContext) IsSysAdmin() bool {
|
||||
func (f *fakeSecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
}
|
||||
func (f *fakeSecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
|
||||
return false
|
||||
}
|
||||
func (f *fakeSecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
|
||||
return false
|
||||
}
|
||||
func (f *fakeSecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
|
||||
func (f *fakeSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
return false
|
||||
}
|
||||
func (f *fakeSecurityContext) GetMyProjects() ([]*models.Project, error) {
|
||||
|
@ -12,7 +12,7 @@
|
||||
@include text-overflow;
|
||||
}
|
||||
|
||||
@mixin grid-left-top-pos{
|
||||
@mixin grid-right-top-pos{
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 35px;
|
||||
|
@ -1,47 +1,62 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, Inject } from '@angular/core';
|
||||
import { Http } from '@angular/http';
|
||||
import { throwError as observableThrowError, Observable } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from "../../service.config";
|
||||
|
||||
export abstract class GcApiRepository {
|
||||
abstract postSchedule(param): Observable<any>;
|
||||
|
||||
abstract putSchedule(param): Observable<any>;
|
||||
|
||||
abstract getSchedule(): Observable<any>;
|
||||
|
||||
abstract getLog(id): Observable<any>;
|
||||
|
||||
abstract getStatus(id): Observable<any>;
|
||||
|
||||
abstract getJobs(): Observable<any>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GcApiRepository {
|
||||
|
||||
export class GcApiDefaultRepository extends GcApiRepository {
|
||||
constructor(
|
||||
private http: Http,
|
||||
@Inject(SERVICE_CONFIG) private config: IServiceConfig
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public postSchedule(param): Observable<any> {
|
||||
return this.http.post("/api/system/gc/schedule", param)
|
||||
return this.http.post(`${this.config.gcEndpoint}/schedule`, param)
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
|
||||
public putSchedule(param): Observable<any> {
|
||||
return this.http.put("/api/system/gc/schedule", param)
|
||||
return this.http.put(`${this.config.gcEndpoint}/schedule`, param)
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
|
||||
public getSchedule(): Observable<any> {
|
||||
return this.http.get("/api/system/gc/schedule")
|
||||
return this.http.get(`${this.config.gcEndpoint}/schedule`)
|
||||
.pipe(catchError(error => observableThrowError(error)))
|
||||
.pipe(map(response => response.json()));
|
||||
}
|
||||
|
||||
public getLog(id): Observable<any> {
|
||||
return this.http.get("/api/system/gc/" + id + "/log")
|
||||
return this.http.get(`${this.config.gcEndpoint}/${id}/log`)
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
|
||||
public getStatus(id): Observable<any> {
|
||||
return this.http.get("/api/system/gc/" + id)
|
||||
return this.http.get(`${this.config.gcEndpoint}/id`)
|
||||
.pipe(catchError(error => observableThrowError(error)))
|
||||
.pipe(map(response => response.json()));
|
||||
}
|
||||
|
||||
public getJobs(): Observable<any> {
|
||||
return this.http.get("/api/system/gc")
|
||||
return this.http.get(`${this.config.gcEndpoint}`)
|
||||
.pipe(catchError(error => observableThrowError(error)))
|
||||
.pipe(map(response => response.json()));
|
||||
}
|
44
src/portal/lib/src/config/gc/gc.component.spec.ts
Normal file
44
src/portal/lib/src/config/gc/gc.component.spec.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { GcComponent } from './gc.component';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../../service.config';
|
||||
import { GcApiRepository, GcApiDefaultRepository} from './gc.api.repository';
|
||||
import { GcRepoService } from './gc.service';
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { ErrorHandler } from '../../error-handler/error-handler';
|
||||
import { GcViewModelFactory } from './gc.viewmodel.factory';
|
||||
import { GcUtility } from './gc.utility';
|
||||
|
||||
describe('GcComponent', () => {
|
||||
let component: GcComponent;
|
||||
let fixture: ComponentFixture<GcComponent>;
|
||||
let config: IServiceConfig = {
|
||||
systemInfoEndpoint: "/api/system/gc"
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule
|
||||
],
|
||||
declarations: [ GcComponent ],
|
||||
providers: [
|
||||
{ provide: GcApiRepository, useClass: GcApiDefaultRepository },
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
GcRepoService,
|
||||
ErrorHandler,
|
||||
GcViewModelFactory,
|
||||
GcUtility
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GcComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -5,7 +5,7 @@ import { GcViewModelFactory } from "./gc.viewmodel.factory";
|
||||
import { GcRepoService } from "./gc.service";
|
||||
import { WEEKDAYS, SCHEDULE_TYPE, ONE_MINITUE, THREE_SECONDS} from './gc.const';
|
||||
import { GcUtility } from './gc.utility';
|
||||
import { ErrorHandler } from '@harbor/ui';
|
||||
import { ErrorHandler } from '../../error-handler/index';
|
||||
|
||||
@Component({
|
||||
selector: 'gc-config',
|
@ -3,9 +3,10 @@ import { Http } from '@angular/http';
|
||||
import { Observable, Subscription, Subject, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { GcApiRepository } from './gc.api.repository';
|
||||
import { ErrorHandler } from '@harbor/ui';
|
||||
import { ErrorHandler } from '../../error-handler/index';
|
||||
import { GcJobData } from './gcLog';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class GcRepoService {
|
||||
|
7
src/portal/lib/src/config/gc/index.ts
Normal file
7
src/portal/lib/src/config/gc/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from "./gc.component";
|
||||
export * from "./gc.const";
|
||||
export * from "./gc.api.repository";
|
||||
export * from "./gc.service";
|
||||
export * from "./gc.utility";
|
||||
export * from "./gc.viewmodel.factory";
|
||||
export * from "./gcLog";
|
@ -4,15 +4,19 @@ import { ReplicationConfigComponent } from './replication/replication-config.com
|
||||
import { SystemSettingsComponent } from './system/system-settings.component';
|
||||
import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component';
|
||||
import { RegistryConfigComponent } from './registry-config.component';
|
||||
import { GcComponent } from './gc/gc.component';
|
||||
|
||||
|
||||
export * from './config';
|
||||
export * from './replication/replication-config.component';
|
||||
export * from './system/system-settings.component';
|
||||
export * from './vulnerability/vulnerability-config.component';
|
||||
export * from './registry-config.component';
|
||||
export * from './gc/index';
|
||||
|
||||
export const CONFIGURATION_DIRECTIVES: Type<any>[] = [
|
||||
ReplicationConfigComponent,
|
||||
GcComponent,
|
||||
SystemSettingsComponent,
|
||||
VulnerabilityConfigComponent,
|
||||
RegistryConfigComponent
|
||||
|
@ -13,5 +13,11 @@
|
||||
<vulnerability-config *ngIf="withClair" #vulnerabilityConfig [(vulnerabilityConfig)]="config" [showSubTitle]="true"></vulnerability-config>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button id="config-gc" clrTabLink>{{'CONFIG.GC' | translate}}</button>
|
||||
<clr-tab-content id="gc" *clrIfActive>
|
||||
<gc-config #gcConfig></gc-config>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
|
@ -8,6 +8,7 @@ import { SystemSettingsComponent } from './system/system-settings.component';
|
||||
import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component';
|
||||
import { RegistryConfigComponent } from './registry-config.component';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import { GcComponent } from './gc/gc.component';
|
||||
|
||||
import {
|
||||
ConfigurationService,
|
||||
@ -62,7 +63,8 @@ describe('RegistryConfigComponent (inline template)', () => {
|
||||
SystemSettingsComponent,
|
||||
VulnerabilityConfigComponent,
|
||||
RegistryConfigComponent,
|
||||
ConfirmationDialogComponent
|
||||
ConfirmationDialogComponent,
|
||||
GcComponent
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
clone
|
||||
} from '../utils';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
import { SystemSettingsComponent, VulnerabilityConfigComponent} from './index';
|
||||
import { SystemSettingsComponent, VulnerabilityConfigComponent, GcComponent} from './index';
|
||||
import { Configuration } from './config';
|
||||
|
||||
@Component({
|
||||
@ -30,6 +30,7 @@ export class RegistryConfigComponent implements OnInit {
|
||||
|
||||
@ViewChild("systemSettings") systemSettings: SystemSettingsComponent;
|
||||
@ViewChild("vulnerabilityConfig") vulnerabilityCfg: VulnerabilityConfigComponent;
|
||||
@ViewChild("gc") gc: GcComponent;
|
||||
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
|
||||
|
||||
constructor(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, Input, Output, EventEmitter, ViewChild, Inject, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { Configuration, StringValueItem } from '../config';
|
||||
import { SERVICE_CONFIG, IServiceConfig, downloadUrl } from '../../service.config';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../../service.config';
|
||||
import { clone, isEmpty, getChanges, toPromise } from '../../utils';
|
||||
import { ErrorHandler } from '../../error-handler/index';
|
||||
import { ConfirmationMessage } from '../../confirmation-dialog/confirmation-message';
|
||||
@ -23,7 +23,7 @@ export class SystemSettingsComponent implements OnChanges {
|
||||
config: Configuration = new Configuration();
|
||||
onGoing = false;
|
||||
private originalConfig: Configuration;
|
||||
downloadLink: string = downloadUrl;
|
||||
downloadLink: string;
|
||||
@Output() configChange: EventEmitter<Configuration> = new EventEmitter<Configuration>();
|
||||
@Output() readOnlyChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
@Output() reloadSystemConfig: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user