Merge remote-tracking branch 'upstream/master' into 190215_syncmaster

This commit is contained in:
Wenkai Yin 2019-02-15 14:15:00 +08:00
commit 4ceaf3e6c9
231 changed files with 7096 additions and 1567 deletions

View File

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

View File

@ -95,7 +95,7 @@ VERSIONFILEPATH=$(CURDIR)
VERSIONFILENAME=UIVERSION
#versions
REGISTRYVERSION=v2.6.2
REGISTRYVERSION=v2.7.1
NGINXVERSION=$(VERSIONTAG)
NOTARYVERSION=v0.6.1
CLAIRVERSION=v2.0.7

View File

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

View File

@ -19,6 +19,18 @@ securityDefinitions:
security:
- basicAuth: []
paths:
/health:
get:
summary: 'Health check API'
description: |
The endpoint returns the health stauts of the system.
tags:
- Products
responses:
'200':
description: The system health status.
schema:
$ref: '#/definitions/OverallHealthStatus'
/search:
get:
summary: 'Search for projects, repositories and helm charts'
@ -705,6 +717,37 @@ paths:
$ref: '#/definitions/User'
'401':
description: User need to log in first.
/users/current/permissions:
get:
summary: Get current user permissions.
description: |
This endpoint is to get the current user permissions.
parameters:
- name: scope
in: query
type: string
required: false
description: Get permissions of the scope
- name: relative
in: query
type: boolean
required: false
description: |
If true, the resources in the response are relative to the scope,
eg for resource '/project/1/repository' if relative is 'true' then the resource in response will be 'repository'.
tags:
- Products
responses:
'200':
description: Get current user permission successfully.
schema:
type: array
items:
$ref: '#/definitions/Permission'
'401':
description: User need to log in first.
'500':
description: Internal errors.
'/users/{user_id}':
get:
summary: Get a user's profile.
@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -15,6 +15,7 @@
package dao
import (
"errors"
"fmt"
"strconv"
"strings"
@ -32,6 +33,9 @@ const (
ClairDBAlias = "clair-db"
)
// ErrDupRows is returned by DAO when inserting failed with error "duplicate key value violates unique constraint"
var ErrDupRows = errors.New("sql: duplicate row in DB")
// Database is an interface of different databases
type Database interface {
// Name returns the name of database

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

@ -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")
}

View File

@ -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.

View 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{})
}

View 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)
}
}
*/

View 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)
}

View File

@ -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"

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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"

View File

@ -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")

View File

@ -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"
}

View File

@ -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) {

View File

@ -12,7 +12,7 @@
@include text-overflow;
}
@mixin grid-left-top-pos{
@mixin grid-right-top-pos{
position: absolute;
z-index: 100;
right: 35px;

View File

@ -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()));
}

View 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();
});
});

View File

@ -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',

View File

@ -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 {

View 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";

View File

@ -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

View File

@ -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>

View File

@ -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,

View File

@ -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(

View File

@ -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