author Wang Yan <wangyan@vmware.com> 1605849192 +0800
committer Wang Yan <wangyan@vmware.com> 1606361046 +0800

update code per review comments

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2020-11-20 13:13:12 +08:00
parent 8e61a3ea31
commit 02846194e0
40 changed files with 807 additions and 2146 deletions

View File

@ -1990,187 +1990,6 @@ 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: page
in: query
type: integer
format: int32
required: false
description: The page number.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page.
- 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'
headers:
X-Total-Count:
description: The total count of available items
type: integer
Link:
description: Link to previous page and next page
type: string
'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'
headers:
Location:
type: string
description: The URL of the created resource
'400':
description: Project id is not valid.
'401':
description: User need to log in first.
'403':
description: User in session does not have permission to the project.
'409':
description: An robot account with same name already exist in the project.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/robots/{robot_id}':
get:
summary: Return the infor of the specified robot account.
description: Return the infor of the specified robot account.
tags:
- Products
- Robot Account
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: robot_id
in: path
type: integer
format: int64
required: true
description: The ID of robot account.
responses:
'200':
description: Robot account information.
schema:
$ref: '#/definitions/RobotAccount'
'401':
description: User need to log in first.
'403':
description: User in session does not have permission to the project.
'404':
description: The robot account is not found.
'500':
description: Unexpected internal errors.
put:
summary: Update status of robot account.
description: Used to disable/enable a specified robot account.
tags:
- Products
- Robot Account
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: robot_id
in: path
type: integer
format: int64
required: true
description: The ID of robot account.
- name: robot
in: body
description: Request body of enable/disable a robot account.
required: true
schema:
$ref: '#/definitions/RobotAccountUpdate'
responses:
'200':
description: Robot account has been modified success.
'500':
description: Unexpected internal errors.
delete:
summary: Delete the specified robot account
description: Delete the specified robot account
tags:
- Products
- Robot Account
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: robot_id
in: path
type: integer
format: int64
required: true
description: The ID of robot account.
responses:
'200':
description: The specified robot account is successfully deleted.
'401':
description: User need to log in first.
'403':
description: User in session does not have permission to the project.
'404':
description: The robot account is not found.
'500':
description: Unexpected internal errors.
'/system/oidc/ping':
post:
summary: Test the OIDC endpoint.
@ -4633,75 +4452,6 @@ definitions:
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
expires_at:
type: integer
description: The expiration of robot account (in seconds)
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
expires_at:
type: integer
description: The expiration time on or after which the JWT MUST NOT be accepted for processing.
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:
disabled:
type: boolean
description: The robot account is disable or enable
Permission:
type: object
description: The permission

View File

@ -23,20 +23,21 @@ import (
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/pkg/robot2/model"
)
// SecurityContext implements security.Context interface based on database
type SecurityContext struct {
robot *model.Robot
ctl project.Controller
policy []*types.Policy
evaluator evaluator.Evaluator
once sync.Once
robot *model.Robot
isSystemLevel bool
ctl project.Controller
policy []*types.Policy
evaluator evaluator.Evaluator
once sync.Once
}
// NewSecurityContext ...
func NewSecurityContext(robot *model.Robot, policy []*types.Policy) *SecurityContext {
func NewSecurityContext(robot *model.Robot, isSystemLevel bool, policy []*types.Policy) *SecurityContext {
return &SecurityContext{
ctl: project.Ctl,
robot: robot,
@ -76,7 +77,11 @@ func (s *SecurityContext) IsSolutionUser() bool {
// Can returns whether the robot can do action on resource
func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource types.Resource) bool {
s.once.Do(func() {
s.evaluator = rbac.NewProjectEvaluator(s.ctl, rbac.NewBuilderForPolicies(s.GetUsername(), s.policy, filterRobotPolicies))
if s.isSystemLevel {
s.evaluator = rbac.NewProjectEvaluator(s.ctl, rbac.NewBuilderForPolicies(s.GetUsername(), s.policy))
} else {
s.evaluator = rbac.NewProjectEvaluator(s.ctl, rbac.NewBuilderForPolicies(s.GetUsername(), s.policy, filterRobotPolicies))
}
})
return s.evaluator != nil && s.evaluator.HasPermission(ctx, resource, action)

View File

@ -23,7 +23,7 @@ import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/pkg/robot2/model"
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/stretchr/testify/assert"
@ -38,45 +38,45 @@ var (
func TestIsAuthenticated(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil)
ctx := NewSecurityContext(nil, false, nil)
assert.False(t, ctx.IsAuthenticated())
// authenticated
ctx = NewSecurityContext(&model.Robot{
Name: "test",
Disabled: false,
}, nil)
}, false, nil)
assert.True(t, ctx.IsAuthenticated())
}
func TestGetUsername(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil)
ctx := NewSecurityContext(nil, false, nil)
assert.Equal(t, "", ctx.GetUsername())
// authenticated
ctx = NewSecurityContext(&model.Robot{
Name: "test",
Disabled: false,
}, nil)
}, false, nil)
assert.Equal(t, "test", ctx.GetUsername())
}
func TestIsSysAdmin(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil)
ctx := NewSecurityContext(nil, false, nil)
assert.False(t, ctx.IsSysAdmin())
// authenticated, non admin
ctx = NewSecurityContext(&model.Robot{
Name: "test",
Disabled: false,
}, nil)
}, false, nil)
assert.False(t, ctx.IsSysAdmin())
}
func TestIsSolutionUser(t *testing.T) {
ctx := NewSecurityContext(nil, nil)
ctx := NewSecurityContext(nil, false, nil)
assert.False(t, ctx.IsSolutionUser())
}
@ -95,7 +95,7 @@ func TestHasPullPerm(t *testing.T) {
ctl := &projecttesting.Controller{}
mock.OnAnything(ctl, "Get").Return(private, nil)
ctx := NewSecurityContext(robot, policies)
ctx := NewSecurityContext(robot, false, policies)
ctx.ctl = ctl
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPull, resource))
@ -116,7 +116,7 @@ func TestHasPushPerm(t *testing.T) {
ctl := &projecttesting.Controller{}
mock.OnAnything(ctl, "Get").Return(private, nil)
ctx := NewSecurityContext(robot, policies)
ctx := NewSecurityContext(robot, false, policies)
ctx.ctl = ctl
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPush, resource))
@ -141,7 +141,7 @@ func TestHasPushPullPerm(t *testing.T) {
ctl := &projecttesting.Controller{}
mock.OnAnything(ctl, "Get").Return(private, nil)
ctx := NewSecurityContext(robot, policies)
ctx := NewSecurityContext(robot, false, policies)
ctx.ctl = ctl
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPush, resource) && ctx.Can(context.TODO(), rbac.ActionPull, resource))

View File

@ -105,7 +105,7 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, error) {
if err != nil {
return 0, err
}
r.ID = robotID
if err := d.createPermission(ctx, r); err != nil {
return 0, err
}
@ -261,6 +261,7 @@ func (d *controller) populatePermissions(ctx context.Context, r *Robot) error {
log.Errorf("failed to decode scope of robot %d: %v", r.ID, err)
return err
}
p.Scope = scope
p.Kind = kind
p.Namespace = namespace
p.Access = accesses

View File

@ -42,6 +42,7 @@ type Permission struct {
Kind string `json:"kind"`
Namespace string `json:"namespace"`
Access []*types.Policy `json:"access"`
Scope string `json:"-"`
}
// Option ...

View File

@ -18,20 +18,21 @@ import (
"bytes"
"context"
"fmt"
"github.com/goharbor/harbor/src/controller/project"
"sync"
cj "github.com/goharbor/harbor/src/common/job"
jm "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/rbac"
ar "github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/robot"
sc "github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/pkg/robot2/model"
sca "github.com/goharbor/harbor/src/pkg/scan"
"github.com/goharbor/harbor/src/pkg/scan/all"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
@ -70,6 +71,8 @@ type basicController struct {
sc sc.Controller
// Robot account controller
rc robot.Controller
// Project controller
pro project.Controller
// Job service client
jc jcGetter
// UUID generator
@ -88,7 +91,9 @@ func NewController() Controller {
// Refer to the default scanner controller
sc: sc.DefaultController,
// Refer to the default robot account controller
rc: robot.RobotCtr,
rc: robot.Ctl,
// Refer to the default project controller
pro: project.Ctl,
// Refer to the default job service client
jc: func() cj.Client {
return cj.GlobalClient
@ -282,7 +287,7 @@ func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner
}
func (bc *basicController) scanArtifact(ctx context.Context, r *scanner.Registration, artifact *ar.Artifact, trackID string, producesMimes []string) error {
jobID, err := bc.launchScanJob(trackID, artifact, r, producesMimes)
jobID, err := bc.launchScanJob(ctx, trackID, artifact, r, producesMimes)
if err != nil {
// Update the status to the concrete error
// Change status code to normal error code
@ -494,7 +499,7 @@ func (bc *basicController) GetScanLog(uuid string) ([]byte, error) {
}
// HandleJobHooks ...
func (bc *basicController) HandleJobHooks(trackID string, change *job.StatusChange) error {
func (bc *basicController) HandleJobHooks(ctx context.Context, trackID string, change *job.StatusChange) error {
if len(trackID) == 0 {
return errors.New("empty track ID")
}
@ -514,7 +519,7 @@ func (bc *basicController) HandleJobHooks(trackID string, change *job.StatusChan
}
if r.ID > 0 {
if err := robot.RobotCtr.DeleteRobotAccount(r.ID); err != nil {
if err := robot.Ctl.Delete(ctx, r.ID); err != nil {
// Should not block the main flow, just logged
log.Error(errors.Wrap(err, "scan controller: handle job hook"))
} else {
@ -578,34 +583,59 @@ func (bc *basicController) GetStats(requester string) (*all.Stats, error) {
}
// makeRobotAccount creates a robot account based on the arguments for scanning.
func (bc *basicController) makeRobotAccount(projectID int64, repository string, registration *scanner.Registration) (*model.Robot, error) {
func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64, repository string, registration *scanner.Registration) (*robot.Robot, error) {
// Use uuid as name to avoid duplicated entries.
UUID, err := bc.uuid()
if err != nil {
return nil, errors.Wrap(err, "scan controller: make robot account")
}
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository)
robotReq := &model.RobotCreate{
Name: fmt.Sprintf("%s-%s", registration.Name, UUID),
Description: "for scan",
ProjectID: projectID,
Access: []*types.Policy{
{Resource: resource, Action: rbac.ActionPull},
{Resource: resource, Action: rbac.ActionScannerPull},
},
}
rb, err := bc.rc.CreateRobotAccount(robotReq)
p, err := bc.pro.Get(ctx, projectID)
if err != nil {
return nil, errors.Wrap(err, "scan controller: make robot account")
}
return rb, nil
robotReq := &robot.Robot{
Robot: model.Robot{
Name: fmt.Sprintf("%s-%s", registration.Name, UUID),
Description: "for scan",
ProjectID: projectID,
ExpiresAt: -1,
},
Level: robot.LEVELPROJECT,
Permissions: []*robot.Permission{
{
Kind: "project",
Namespace: p.Name,
Access: []*types.Policy{
{
Resource: rbac.ResourceRepository,
Action: rbac.ActionPull,
},
{
Resource: rbac.ResourceRepository,
Action: rbac.ActionScannerPull,
},
},
},
},
}
rb, err := bc.rc.Create(ctx, robotReq)
if err != nil {
return nil, errors.Wrap(err, "scan controller: make robot account")
}
r, err := bc.rc.Get(ctx, rb, &robot.Option{WithPermission: false})
if err != nil {
return nil, errors.Wrap(err, "scan controller: make robot account")
}
return r, nil
}
// launchScanJob launches a job to run scan
func (bc *basicController) launchScanJob(trackID string, artifact *ar.Artifact, registration *scanner.Registration, mimes []string) (jobID string, err error) {
func (bc *basicController) launchScanJob(ctx context.Context, trackID string, artifact *ar.Artifact, registration *scanner.Registration, mimes []string) (jobID string, err error) {
var ck string
if registration.UseInternalAddr {
ck = configCoreInternalAddr
@ -618,7 +648,7 @@ func (bc *basicController) launchScanJob(trackID string, artifact *ar.Artifact,
return "", errors.Wrap(err, "scan controller: launch scan job")
}
robot, err := bc.makeRobotAccount(artifact.ProjectID, artifact.RepositoryName, registration)
robot, err := bc.makeRobotAccount(ctx, artifact.ProjectID, artifact.RepositoryName, registration)
if err != nil {
return "", errors.Wrap(err, "scan controller: launch scan job")
}

View File

@ -19,6 +19,8 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
models "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/robot"
"testing"
"time"
@ -27,15 +29,16 @@ import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/pkg/robot2/model"
sca "github.com/goharbor/harbor/src/pkg/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
robottesting "github.com/goharbor/harbor/src/testing/controller/robot"
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
mocktesting "github.com/goharbor/harbor/src/testing/mock"
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report"
@ -166,27 +169,48 @@ func (suite *ControllerTestSuite) SetupSuite() {
mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
suite.reportMgr = mgr
rc := &MockRobotController{}
resource := fmt.Sprintf("/project/%d/repository", suite.artifact.ProjectID)
access := []*types.Policy{
{Resource: types.Resource(resource), Action: rbac.ActionPull},
{Resource: types.Resource(resource), Action: rbac.ActionScannerPull},
}
rc := &robottesting.Controller{}
rname := fmt.Sprintf("%s-%s", suite.registration.Name, "the-uuid-123")
account := &model.RobotCreate{
Name: rname,
Description: "for scan",
ProjectID: suite.artifact.ProjectID,
Access: access,
account := &robot.Robot{
Robot: model.Robot{
Name: rname,
Description: "for scan",
ProjectID: suite.artifact.ProjectID,
ExpiresAt: -1,
},
Level: robot.LEVELPROJECT,
Permissions: []*robot.Permission{
{
Kind: "project",
Namespace: "library",
Access: []*types.Policy{
{
Resource: "repository",
Action: rbac.ActionPull,
},
{
Resource: "repository",
Action: rbac.ActionScannerPull,
},
},
},
},
}
rc.On("CreateRobotAccount", account).Return(&model.Robot{
ID: 1,
Name: rname,
Token: "robot-account",
Description: "for scan",
ProjectID: suite.artifact.ProjectID,
rc.On("Create", context.TODO(), account).Return(int64(1), nil)
rc.On("Get", context.TODO(), int64(1), &robot.Option{
WithPermission: false,
}).Return(&robot.Robot{
Robot: model.Robot{
ID: 1,
Name: rname,
Secret: "robot-account",
Description: "for scan",
ProjectID: suite.artifact.ProjectID,
},
Level: "project",
}, nil)
// Set job parameters
@ -208,7 +232,8 @@ func (suite *ControllerTestSuite) SetupSuite() {
regJSON, err := suite.registration.ToJSON()
require.NoError(suite.T(), err)
rb, _ := rc.CreateRobotAccount(account)
id, _ := rc.Create(context.TODO(), account)
rb, _ := rc.Get(context.TODO(), id, &robot.Option{WithPermission: false})
robotJSON, err := rb.ToJSON()
require.NoError(suite.T(), err)
@ -237,6 +262,12 @@ func (suite *ControllerTestSuite) SetupSuite() {
walkFn(suite.artifact)
})
proCtl := &projecttesting.Controller{}
proCtl.On("Get", context.TODO(), suite.artifact.ProjectID).Return(&models.Project{
ProjectID: suite.artifact.ProjectID,
Name: "library",
}, nil)
suite.c = &basicController{
manager: mgr,
ar: suite.ar,
@ -244,7 +275,8 @@ func (suite *ControllerTestSuite) SetupSuite() {
jc: func() cj.Client {
return jc
},
rc: rc,
rc: rc,
pro: proCtl,
uuid: func() (string, error) {
return "the-uuid-123", nil
},
@ -369,7 +401,7 @@ func (suite *ControllerTestSuite) TestScanControllerHandleJobHooks() {
},
}
err = suite.c.HandleJobHooks("the-uuid-123", statusChange)
err = suite.c.HandleJobHooks(context.TODO(), "the-uuid-123", statusChange)
require.NoError(suite.T(), err)
}
@ -412,52 +444,3 @@ func (mjc *MockJobServiceClient) GetExecutions(uuid string) ([]job.Stats, error)
return args.Get(0).([]job.Stats), args.Error(1)
}
// MockRobotController ...
type MockRobotController struct {
mock.Mock
}
// GetRobotAccount ...
func (mrc *MockRobotController) GetRobotAccount(id int64) (*model.Robot, error) {
args := mrc.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.Robot), args.Error(1)
}
// CreateRobotAccount ...
func (mrc *MockRobotController) CreateRobotAccount(robotReq *model.RobotCreate) (*model.Robot, error) {
args := mrc.Called(robotReq)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.Robot), args.Error(1)
}
// DeleteRobotAccount ...
func (mrc *MockRobotController) DeleteRobotAccount(id int64) error {
args := mrc.Called(id)
return args.Error(0)
}
// UpdateRobotAccount ...
func (mrc *MockRobotController) UpdateRobotAccount(r *model.Robot) error {
args := mrc.Called(r)
return args.Error(0)
}
// ListRobotAccount ...
func (mrc *MockRobotController) ListRobotAccount(query *q.Query) ([]*model.Robot, error) {
args := mrc.Called(query)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*model.Robot), args.Error(1)
}

View File

@ -80,7 +80,7 @@ type Controller interface {
//
// Returns:
// error : non nil error if any errors occurred
HandleJobHooks(trackID string, change *job.StatusChange) error
HandleJobHooks(ctx context.Context, trackID string, change *job.StatusChange) error
// Delete the reports related with the specified digests
//

View File

@ -133,9 +133,6 @@ func init() {
beego.Router("/api/system/CVEAllowlist", &SysCVEAllowlistAPI{}, "get:Get;put:Put")
beego.Router("/api/system/oidc/ping", &OIDCAPI{}, "post:Ping")
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")
beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List")
beego.Router("/api/replication/policies", &ReplicationPolicyAPI{}, "get:List;post:Create")

View File

@ -1,237 +0,0 @@
// 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/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/robot"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
// RobotAPI ...
type RobotAPI struct {
BaseController
project *models.Project
ctr robot.Controller
robot *model.Robot
}
// Prepare ...
func (r *RobotAPI) Prepare() {
r.BaseController.Prepare()
if !r.SecurityCtx.IsAuthenticated() {
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
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.SendBadRequestError(errors.New(errMsg))
return
}
project, err := r.ProjectCtl.Get(r.Context(), pid)
if err != nil {
if errors.IsNotFoundErr(err) {
r.SendNotFoundError(fmt.Errorf("project %d not found", pid))
} else {
r.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err)
}
return
}
r.project = project
r.ctr = robot.RobotCtr
if r.ParamExistsInPath(":id") {
id, err := r.GetInt64FromPath(":id")
if err != nil || id <= 0 {
r.SendBadRequestError(fmt.Errorf("invalid robot ID %s", r.GetStringFromPath(":id")))
return
}
robot, err := r.ctr.GetRobotAccount(id)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to get robot %d: %v", id, err))
return
}
if robot == nil {
r.SendNotFoundError(fmt.Errorf("robot %d not found", id))
return
}
if robot.ProjectID != pid {
r.SendNotFoundError(fmt.Errorf("robot %d not found in project %d", id, pid))
return
}
r.robot = robot
}
}
func (r *RobotAPI) requireAccess(action rbac.Action) bool {
return r.RequireProjectAccess(r.project.ProjectID, action, rbac.ResourceRobot)
}
// Post ...
func (r *RobotAPI) Post() {
if !r.requireAccess(rbac.ActionCreate) {
return
}
var robotReq model.RobotCreate
isValid, err := r.DecodeJSONReqAndValidate(&robotReq)
if !isValid {
r.SendBadRequestError(err)
return
}
robotReq.Visible = true
robotReq.ProjectID = r.project.ProjectID
if err := validateRobotReq(r.project, &robotReq); err != nil {
r.SendBadRequestError(err)
return
}
robot, err := r.ctr.CreateRobotAccount(&robotReq)
if err != nil {
if err == dao.ErrDupRows {
r.SendConflictError(errors.New("conflict robot account"))
return
}
r.SendInternalServerError(errors.Wrap(err, "robot API: post"))
return
}
w := r.Ctx.ResponseWriter
w.Header().Set("Content-Type", "application/json")
robotRep := model.RobotRep{
Name: robot.Name,
Token: robot.Token,
}
r.Redirect(http.StatusCreated, strconv.FormatInt(robot.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
}
keywords := make(map[string]interface{})
keywords["ProjectID"] = r.project.ProjectID
keywords["Visible"] = true
query := &q.Query{
Keywords: keywords,
}
robots, err := r.ctr.ListRobotAccount(query)
if err != nil {
r.SendInternalServerError(errors.Wrap(err, "robot API: list"))
return
}
count := len(robots)
page, size, err := r.GetPaginationParams()
if err != nil {
r.SendBadRequestError(err)
return
}
r.SetPaginationHeader(int64(count), page, size)
r.Data["json"] = robots
r.ServeJSON()
}
// Get get robot by id
func (r *RobotAPI) Get() {
if !r.requireAccess(rbac.ActionRead) {
return
}
r.Data["json"] = r.robot
r.ServeJSON()
}
// Put disable or enable a robot account
func (r *RobotAPI) Put() {
if !r.requireAccess(rbac.ActionUpdate) {
return
}
var robotReq model.RobotCreate
if err := r.DecodeJSONReq(&robotReq); err != nil {
r.SendBadRequestError(err)
return
}
r.robot.Disabled = robotReq.Disabled
if err := r.ctr.UpdateRobotAccount(r.robot); err != nil {
r.SendInternalServerError(errors.Wrap(err, "robot API: update"))
return
}
}
// Delete delete robot by id
func (r *RobotAPI) Delete() {
if !r.requireAccess(rbac.ActionDelete) {
return
}
if err := r.ctr.DeleteRobotAccount(r.robot.ID); err != nil {
r.SendInternalServerError(errors.Wrap(err, "robot API: delete"))
return
}
}
func validateRobotReq(p *models.Project, robotReq *model.RobotCreate) error {
if len(robotReq.Access) == 0 {
return errors.New("access required")
}
policies := rbac.GetPoliciesOfProject(p.ProjectID)
mp := map[string]bool{}
for _, policy := range policies {
mp[policy.String()] = true
}
for _, policy := range robotReq.Access {
if !mp[policy.String()] {
return fmt.Errorf("%s action of %s resource not exist in project %s", policy.Action, policy.Resource, p.Name)
}
}
return nil
}

View File

@ -1,459 +0,0 @@
// 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"
"testing"
"time"
"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/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
var (
robotPath = "/api/projects/1/robots"
robotID int64
)
func TestRobotAPIPost(t *testing.T) {
res := types.Resource("/project/1")
rbacPolicy := &types.Policy{
Resource: res.Subresource(rbac.ResourceRepository),
Action: "pull",
}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
tokenDuration := time.Duration(30) * time.Minute
expiresAt := time.Now().UTC().Add(tokenDuration).Unix()
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 201
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "test",
Description: "test desc",
ExpiresAt: expiresAt,
Access: policies,
},
credential: projAdmin4Robot,
},
code: http.StatusCreated,
},
// 400
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "testIllgel#",
Description: "test desc",
ExpiresAt: expiresAt,
},
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "test not set expiration",
Description: "test desc",
ExpiresAt: -100,
},
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "test",
Description: "resource not exist",
ExpiresAt: expiresAt,
Access: []*types.Policy{
{Resource: res.Subresource("foo"), Action: rbac.ActionCreate},
},
},
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "test",
Description: "action not exist",
ExpiresAt: expiresAt,
Access: []*types.Policy{
{Resource: res.Subresource(rbac.ResourceRepository), Action: "foo"},
},
},
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "test",
Description: "policy not exit",
ExpiresAt: expiresAt,
Access: []*types.Policy{
{Resource: res.Subresource(rbac.ResourceMember), Action: rbac.ActionPush},
},
},
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
// 403 -- developer
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "test2",
Description: "test2 desc",
ExpiresAt: expiresAt,
},
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 409
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &model.RobotCreate{
Name: "test",
Description: "test desc",
ExpiresAt: expiresAt,
Access: policies,
},
credential: projAdmin4Robot,
},
code: http.StatusConflict,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIGet(t *testing.T) {
projectID, err := dao.AddProject(models.Project{Name: "robotget", OwnerID: 1})
if err != nil {
t.Errorf("Error occurred when add project: %v", err)
}
defer dao.DeleteProject(projectID)
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,
},
// 404 robot 1 not belong to the project
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("/api/projects/%d/robots/1", projectID),
credential: sysAdmin,
},
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) {
projectID, err := dao.AddProject(models.Project{Name: "robotput", OwnerID: 1})
if err != nil {
t.Errorf("Error occurred when add project: %v", err)
}
defer dao.DeleteProject(projectID)
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,
},
// 404 robot 1 not belong to the project
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("/api/projects/%d/robots/1", projectID),
credential: sysAdmin,
},
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: &model.Robot{
Disabled: true,
},
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIDelete(t *testing.T) {
projectID, err := dao.AddProject(models.Project{Name: "robotdelete", OwnerID: 1})
if err != nil {
t.Errorf("Error occurred when add project: %v", err)
}
defer dao.DeleteProject(projectID)
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,
},
// 404 robot 1 not belong to the project
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("/api/projects/%d/robots/1", projectID),
credential: sysAdmin,
},
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

@ -16,6 +16,7 @@ package jobs
import (
"encoding/json"
"github.com/goharbor/harbor/src/lib/orm"
"time"
"github.com/goharbor/harbor/src/common/job"
@ -129,7 +130,7 @@ func (h *Handler) HandleScan() {
}
}
if err := scan.DefaultController.HandleJobHooks(h.trackID, h.change); err != nil {
if err := scan.DefaultController.HandleJobHooks(orm.Context(), h.trackID, h.change); err != nil {
err = errors.Wrap(err, "scan job hook handler")
log.Error(err)
h.SendInternalServerError(err)

View File

@ -1,135 +0,0 @@
package robot
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/pkg/token"
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
"time"
)
var (
// RobotCtr is a global variable for the default robot account controller implementation
RobotCtr = NewController(NewDefaultRobotAccountManager())
)
// Controller to handle the requests related with robot account
type Controller interface {
// GetRobotAccount ...
GetRobotAccount(id int64) (*model.Robot, error)
// CreateRobotAccount ...
CreateRobotAccount(robotReq *model.RobotCreate) (*model.Robot, error)
// DeleteRobotAccount ...
DeleteRobotAccount(id int64) error
// UpdateRobotAccount ...
UpdateRobotAccount(r *model.Robot) error
// ListRobotAccount ...
ListRobotAccount(query *q.Query) ([]*model.Robot, error)
}
// DefaultAPIController ...
type DefaultAPIController struct {
manager Manager
}
// NewController ...
func NewController(robotMgr Manager) Controller {
return &DefaultAPIController{
manager: robotMgr,
}
}
// GetRobotAccount ...
func (d *DefaultAPIController) GetRobotAccount(id int64) (*model.Robot, error) {
return d.manager.GetRobotAccount(id)
}
// CreateRobotAccount ...
func (d *DefaultAPIController) CreateRobotAccount(robotReq *model.RobotCreate) (*model.Robot, error) {
var deferDel error
createdName := common.RobotPrefix + robotReq.Name
if robotReq.ExpiresAt == 0 {
tokenDuration := time.Duration(config.RobotTokenDuration()) * time.Minute
robotReq.ExpiresAt = time.Now().UTC().Add(tokenDuration).Unix()
}
// first to add a robot account, and get its id.
robot := &model.Robot{
Name: createdName,
Description: robotReq.Description,
ProjectID: robotReq.ProjectID,
ExpiresAt: robotReq.ExpiresAt,
Visible: robotReq.Visible,
}
id, err := d.manager.CreateRobotAccount(robot)
if err != nil {
return nil, err
}
// generate the token, and return it with response data.
// token is not stored in the database.
opt := token.DefaultTokenOptions()
rClaims := &robot_claim.Claim{
TokenID: id,
ProjectID: robotReq.ProjectID,
Access: robotReq.Access,
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().UTC().Unix(),
Issuer: opt.Issuer,
},
}
// "-1" means the robot account is a permanent account, no expiration time set.
// The ExpiresAt claim is optional, so if it's not set, it will still be considered a valid claim
if robot.ExpiresAt != -1 {
rClaims.ExpiresAt = robotReq.ExpiresAt
}
tk, err := token.New(opt, rClaims)
if err != nil {
deferDel = err
return nil, fmt.Errorf("failed to valid parameters to generate token for robot account, %v", err)
}
rawTk, err := tk.Raw()
if err != nil {
deferDel = err
return nil, fmt.Errorf("failed to sign token for robot account, %v", err)
}
defer func(deferDel error) {
if deferDel != nil {
if err := d.manager.DeleteRobotAccount(id); err != nil {
log.Error(errors.Wrap(err, fmt.Sprintf("failed to delete the robot account: %d", id)))
}
}
}(deferDel)
robot.Token = rawTk
robot.ID = id
return robot, nil
}
// DeleteRobotAccount ...
func (d *DefaultAPIController) DeleteRobotAccount(id int64) error {
return d.manager.DeleteRobotAccount(id)
}
// UpdateRobotAccount ...
func (d *DefaultAPIController) UpdateRobotAccount(r *model.Robot) error {
return d.manager.UpdateRobotAccount(r)
}
// ListRobotAccount ...
func (d *DefaultAPIController) ListRobotAccount(query *q.Query) ([]*model.Robot, error) {
return d.manager.ListRobotAccount(query)
}

View File

@ -1,128 +0,0 @@
package robot
import (
"testing"
"time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/test"
core_cfg "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type ControllerTestSuite struct {
suite.Suite
ctr Controller
t *testing.T
assert *assert.Assertions
require *require.Assertions
robotID int64
}
// SetupSuite ...
func (s *ControllerTestSuite) SetupSuite() {
test.InitDatabaseFromEnv()
conf := map[string]interface{}{
common.RobotTokenDuration: "30",
}
core_cfg.InitWithSettings(conf)
s.t = s.T()
s.assert = assert.New(s.t)
s.require = require.New(s.t)
s.ctr = RobotCtr
}
func (s *ControllerTestSuite) TestRobotAccount() {
res := rbac.Resource("/project/1")
rbacPolicy := &types.Policy{
Resource: res.Subresource(rbac.ResourceRepository),
Action: "pull",
}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
tokenDuration := time.Duration(30) * time.Minute
expiresAt := time.Now().UTC().Add(tokenDuration).Unix()
robot1 := &model.RobotCreate{
Name: "robot1",
Description: "TestCreateRobotAccount",
ExpiresAt: expiresAt,
ProjectID: int64(1),
Access: policies,
}
robot, err := s.ctr.CreateRobotAccount(robot1)
s.require.Nil(err)
s.require.Equal(robot.ProjectID, int64(1))
s.require.Equal(robot.Description, "TestCreateRobotAccount")
s.require.NotEmpty(robot.Token)
s.require.Equal(robot.Name, common.RobotPrefix+"robot1")
robotGet, err := s.ctr.GetRobotAccount(robot.ID)
s.require.Nil(err)
s.require.Equal(robotGet.ProjectID, int64(1))
s.require.Equal(robotGet.Description, "TestCreateRobotAccount")
robot.Disabled = true
err = s.ctr.UpdateRobotAccount(robot)
s.require.Nil(err)
s.require.Equal(robot.Disabled, true)
robot2 := &model.RobotCreate{
Name: "robot2",
Description: "TestCreateRobotAccount",
ExpiresAt: -1,
ProjectID: int64(1),
Access: policies,
}
r2, _ := s.ctr.CreateRobotAccount(robot2)
s.robotID = r2.ID
robot3 := &model.RobotCreate{
Name: "robot3",
Description: "TestCreateRobotAccount",
ExpiresAt: expiresAt,
ProjectID: int64(11),
Access: policies,
}
r3, _ := s.ctr.CreateRobotAccount(robot3)
keywords := make(map[string]interface{})
keywords["ProjectID"] = int64(1)
query := &q.Query{
Keywords: keywords,
}
robots, err := s.ctr.ListRobotAccount(query)
s.require.Nil(err)
s.require.Equal(len(robots), 2)
s.require.Equal(robots[1].Name, common.RobotPrefix+"robot2")
err = s.ctr.DeleteRobotAccount(robot.ID)
s.require.Nil(err)
err = s.ctr.DeleteRobotAccount(r3.ID)
s.require.Nil(err)
robots, err = s.ctr.ListRobotAccount(query)
s.require.Equal(len(robots), 1)
}
// TearDownSuite clears env for test suite
func (s *ControllerTestSuite) TearDownSuite() {
err := s.ctr.DeleteRobotAccount(s.robotID)
require.NoError(s.T(), err, "delete robot")
}
// TestController ...
func TestController(t *testing.T) {
suite.Run(t, new(ControllerTestSuite))
}

View File

@ -1,120 +0,0 @@
package dao
import (
"context"
"fmt"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
libOrm "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/robot/model"
"strings"
"time"
)
// RobotAccountDao defines the interface to access the ImmutableRule data model
type RobotAccountDao interface {
// CreateRobotAccount ...
CreateRobotAccount(robot *model.Robot) (int64, error)
// UpdateRobotAccount ...
UpdateRobotAccount(robot *model.Robot) error
// GetRobotAccount ...
GetRobotAccount(id int64) (*model.Robot, error)
// ListRobotAccounts ...
ListRobotAccounts(query *q.Query) ([]*model.Robot, error)
// DeleteRobotAccount ...
DeleteRobotAccount(id int64) error
// DeleteByProjectID ...
DeleteByProjectID(ctx context.Context, projectID int64) error
}
// New creates a default implementation for RobotAccountDao
func New() RobotAccountDao {
return &robotAccountDao{}
}
type robotAccountDao struct{}
// CreateRobotAccount ...
func (r *robotAccountDao) CreateRobotAccount(robot *model.Robot) (int64, error) {
now := time.Now()
robot.CreationTime = now
robot.UpdateTime = now
id, err := dao.GetOrmer().Insert(robot)
if err != nil {
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return 0, dao.ErrDupRows
}
return 0, err
}
return id, nil
}
// GetRobotAccount ...
func (r *robotAccountDao) GetRobotAccount(id int64) (*model.Robot, error) {
robot := &model.Robot{
ID: id,
}
if err := dao.GetOrmer().Read(robot); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return robot, nil
}
// ListRobotAccounts ...
func (r *robotAccountDao) ListRobotAccounts(query *q.Query) ([]*model.Robot, error) {
o := dao.GetOrmer()
qt := o.QueryTable(new(model.Robot))
if query != nil {
if len(query.Keywords) > 0 {
for k, v := range query.Keywords {
if k == "ProjectID" {
qt = qt.Filter("ProjectID", v)
} else {
qt = qt.Filter(fmt.Sprintf("%s__icontains", k), v)
}
}
}
if query.PageNumber > 0 && query.PageSize > 0 {
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
}
robots := make([]*model.Robot, 0)
_, err := qt.All(&robots)
return robots, err
}
// UpdateRobotAccount ...
func (r *robotAccountDao) UpdateRobotAccount(robot *model.Robot) error {
robot.UpdateTime = time.Now()
_, err := dao.GetOrmer().Update(robot)
return err
}
// DeleteRobotAccount ...
func (r *robotAccountDao) DeleteRobotAccount(id int64) error {
_, err := dao.GetOrmer().QueryTable(&model.Robot{}).Filter("ID", id).Delete()
return err
}
// DeleteByProjectID ...
func (r *robotAccountDao) DeleteByProjectID(ctx context.Context, projectID int64) error {
qs, err := libOrm.QuerySetter(ctx, &model.Robot{}, q.New(q.KeyWords{"ProjectID": projectID}))
if err != nil {
return err
}
_, err = qs.Delete()
return err
}

View File

@ -1,173 +0,0 @@
package dao
import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/robot/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
)
type robotAccountDaoTestSuite struct {
htesting.Suite
require *require.Assertions
assert *assert.Assertions
dao RobotAccountDao
id1 int64
id2 int64
id3 int64
id4 int64
}
func (t *robotAccountDaoTestSuite) SetupSuite() {
t.require = require.New(t.T())
t.assert = assert.New(t.T())
dao.PrepareTestForPostgresSQL()
t.dao = New()
}
func (t *robotAccountDaoTestSuite) TestCreateRobotAccount() {
robotName := "test1"
robot := &model.Robot{
Name: robotName,
Description: "test1 description",
ProjectID: 1,
}
id, err := t.dao.CreateRobotAccount(robot)
t.require.Nil(err)
t.id1 = id
t.require.Nil(err)
t.require.NotNil(id)
}
func (t *robotAccountDaoTestSuite) TestGetRobotAccount() {
robotName := "test2"
robot := &model.Robot{
Name: robotName,
Description: "test2 description",
ProjectID: 1,
}
// add
id, err := t.dao.CreateRobotAccount(robot)
t.require.Nil(err)
t.id2 = id
robot, err = t.dao.GetRobotAccount(id)
t.require.Nil(err)
t.require.Equal(robotName, robot.Name)
}
func (t *robotAccountDaoTestSuite) TestListRobotAccounts() {
robotName := "test3"
robot := &model.Robot{
Name: robotName,
Description: "test3 description",
ProjectID: 1,
}
id, err := t.dao.CreateRobotAccount(robot)
t.require.Nil(err)
t.id3 = id
keywords := make(map[string]interface{})
keywords["ProjectID"] = 1
robots, err := t.dao.ListRobotAccounts(&q.Query{
Keywords: keywords,
})
t.require.Nil(err)
t.require.Equal(3, len(robots))
}
func (t *robotAccountDaoTestSuite) TestUpdateRobotAccount() {
robotName := "test4"
robot := &model.Robot{
Name: robotName,
Description: "test4 description",
ProjectID: 1,
}
// add
id, err := t.dao.CreateRobotAccount(robot)
t.require.Nil(err)
t.id4 = id
// Disable
robot.Disabled = true
err = t.dao.UpdateRobotAccount(robot)
t.require.Nil(err)
// Get
robot, err = t.dao.GetRobotAccount(id)
t.require.Nil(err)
t.require.Equal(true, robot.Disabled)
}
func (t *robotAccountDaoTestSuite) TestDeleteRobotAccount() {
robotName := "test5"
robot := &model.Robot{
Name: robotName,
Description: "test5 description",
ProjectID: 1,
}
// add
id, err := t.dao.CreateRobotAccount(robot)
t.require.Nil(err)
// Disable
err = t.dao.DeleteRobotAccount(id)
t.require.Nil(err)
// Get
robot, err = t.dao.GetRobotAccount(id)
t.require.Nil(err)
}
func (t *robotAccountDaoTestSuite) TestDeleteRobotAccountByPID() {
t.WithProject(func(projectID int64, projectName string) {
robot := &model.Robot{
Name: t.RandString(5),
Description: "TestDeleteRobotAccountByPID description",
ProjectID: projectID,
}
_, err := t.dao.CreateRobotAccount(robot)
t.require.Nil(err)
robot = &model.Robot{
Name: t.RandString(5),
Description: "TestDeleteRobotAccountByPID description",
ProjectID: projectID,
}
_, err = t.dao.CreateRobotAccount(robot)
t.require.Nil(err)
// Delete
err = t.dao.DeleteByProjectID(t.Context(), projectID)
t.require.Nil(err)
// Get
keywords := make(map[string]interface{})
keywords["ProjectID"] = projectID
robots, err := t.dao.ListRobotAccounts(&q.Query{
Keywords: keywords,
})
t.require.Nil(err)
t.require.Equal(0, len(robots))
})
}
// TearDownSuite clears env for test suite
func (t *robotAccountDaoTestSuite) TearDownSuite() {
err := t.dao.DeleteRobotAccount(t.id1)
require.NoError(t.T(), err, "delete robot 1")
err = t.dao.DeleteRobotAccount(t.id2)
require.NoError(t.T(), err, "delete robot 2")
err = t.dao.DeleteRobotAccount(t.id3)
require.NoError(t.T(), err, "delete robot 3")
err = t.dao.DeleteRobotAccount(t.id4)
require.NoError(t.T(), err, "delete robot 4")
}
func TestRobotAccountDaoTestSuite(t *testing.T) {
suite.Run(t, &robotAccountDaoTestSuite{})
}

View File

@ -1,75 +0,0 @@
package robot
import (
"context"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/robot/dao"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
var (
// Mgr is a global variable for the default robot account manager implementation
Mgr = NewDefaultRobotAccountManager()
)
// Manager ...
type Manager interface {
// GetRobotAccount ...
GetRobotAccount(id int64) (*model.Robot, error)
// CreateRobotAccount ...
CreateRobotAccount(m *model.Robot) (int64, error)
// DeleteRobotAccount ...
DeleteRobotAccount(id int64) error
// DeleteByProjectID ...
DeleteByProjectID(ctx context.Context, projectID int64) error
// UpdateRobotAccount ...
UpdateRobotAccount(m *model.Robot) error
// ListRobotAccount ...
ListRobotAccount(query *q.Query) ([]*model.Robot, error)
}
type defaultRobotManager struct {
dao dao.RobotAccountDao
}
// NewDefaultRobotAccountManager return a new instance of defaultRobotManager
func NewDefaultRobotAccountManager() Manager {
return &defaultRobotManager{
dao: dao.New(),
}
}
// GetRobotAccount ...
func (drm *defaultRobotManager) GetRobotAccount(id int64) (*model.Robot, error) {
return drm.dao.GetRobotAccount(id)
}
// CreateRobotAccount ...
func (drm *defaultRobotManager) CreateRobotAccount(r *model.Robot) (int64, error) {
return drm.dao.CreateRobotAccount(r)
}
// DeleteRobotAccount ...
func (drm *defaultRobotManager) DeleteRobotAccount(id int64) error {
return drm.dao.DeleteRobotAccount(id)
}
// DeleteByProjectID ...
func (drm *defaultRobotManager) DeleteByProjectID(ctx context.Context, projectID int64) error {
return drm.dao.DeleteByProjectID(ctx, projectID)
}
// UpdateRobotAccount ...
func (drm *defaultRobotManager) UpdateRobotAccount(r *model.Robot) error {
return drm.dao.UpdateRobotAccount(r)
}
// ListRobotAccount ...
func (drm *defaultRobotManager) ListRobotAccount(query *q.Query) ([]*model.Robot, error) {
return drm.dao.ListRobotAccounts(query)
}

View File

@ -1,112 +0,0 @@
package robot
import (
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/robot/dao"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"os"
"testing"
)
type managerTestingSuite struct {
suite.Suite
t *testing.T
assert *assert.Assertions
require *require.Assertions
mockRobotDao *dao.RobotAccountDao
mgr Manager
}
func (m *managerTestingSuite) SetupSuite() {
m.t = m.T()
m.assert = assert.New(m.t)
m.require = require.New(m.t)
err := os.Setenv("RUN_MODE", "TEST")
m.require.Nil(err)
}
func (m *managerTestingSuite) TearDownSuite() {
err := os.Unsetenv("RUN_MODE")
m.require.Nil(err)
}
func (m *managerTestingSuite) SetupTest() {
m.mockRobotDao = &dao.RobotAccountDao{}
m.mgr = &defaultRobotManager{
dao: m.mockRobotDao,
}
}
func TestManagerTestingSuite(t *testing.T) {
suite.Run(t, &managerTestingSuite{})
}
func (m *managerTestingSuite) TestCreateRobotAccount() {
m.mockRobotDao.On("CreateRobotAccount", mock.Anything, mock.Anything).Return(int64(1), nil)
id, err := m.mgr.CreateRobotAccount(&model.Robot{})
m.mockRobotDao.AssertCalled(m.t, "CreateRobotAccount", mock.Anything)
m.require.Nil(err)
m.assert.Equal(int64(1), id)
}
func (m *managerTestingSuite) TestUpdateRobotAccount() {
m.mockRobotDao.On("UpdateRobotAccount", mock.Anything, mock.Anything).Return(nil)
err := m.mgr.UpdateRobotAccount(&model.Robot{})
m.mockRobotDao.AssertCalled(m.t, "UpdateRobotAccount", mock.Anything)
m.require.Nil(err)
}
func (m *managerTestingSuite) TestDeleteRobotAccount() {
m.mockRobotDao.On("DeleteRobotAccount", mock.Anything, mock.Anything).Return(nil)
err := m.mgr.DeleteRobotAccount(int64(1))
m.mockRobotDao.AssertCalled(m.t, "DeleteRobotAccount", mock.Anything)
m.require.Nil(err)
}
func (m *managerTestingSuite) TestGetRobotAccount() {
m.mockRobotDao.On("GetRobotAccount", mock.Anything, mock.Anything).Return(&model.Robot{
ID: 1,
ProjectID: 1,
Disabled: true,
ExpiresAt: 150000,
}, nil)
ir, err := m.mgr.GetRobotAccount(1)
m.mockRobotDao.AssertCalled(m.t, "GetRobotAccount", mock.Anything, mock.Anything)
m.require.Nil(err)
m.require.NotNil(ir)
m.assert.Equal(int64(1), ir.ID)
}
func (m *managerTestingSuite) ListRobotAccount() {
m.mockRobotDao.On("ListRobotAccount", mock.Anything, mock.Anything).Return([]model.Robot{
{
ID: 1,
ProjectID: 1,
Disabled: false,
ExpiresAt: 12345,
},
{
ID: 2,
ProjectID: 1,
Disabled: false,
ExpiresAt: 54321,
}}, nil)
keywords := make(map[string]interface{})
keywords["ProjectID"] = int64(1)
query := &q.Query{
Keywords: keywords,
}
rs, err := m.mgr.ListRobotAccount(query)
m.mockRobotDao.AssertCalled(m.t, "ListRobotAccount", mock.Anything, mock.Anything)
m.require.Nil(err)
m.assert.Equal(len(rs), 2)
m.assert.Equal(rs[0].Disabled, false)
m.assert.Equal(rs[1].ExpiresAt, 54321)
}

View File

@ -1,102 +0,0 @@
package model
import (
"encoding/json"
"errors"
"time"
"github.com/astaxie/beego/orm"
"github.com/astaxie/beego/validation"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// RobotTable is the name of table in DB that holds the robot object
const RobotTable = "robot"
func init() {
orm.RegisterModel(&Robot{})
}
// Robot holds the details of a robot.
type Robot struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
Name string `orm:"column(name)" json:"name"`
Token string `orm:"-" json:"token"`
Description string `orm:"column(description)" json:"description"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
ExpiresAt int64 `orm:"column(expiresat)" json:"expires_at"`
Disabled bool `orm:"column(disabled)" json:"disabled"`
Visible bool `orm:"column(visible)" json:"-"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// TableName ...
func (r *Robot) TableName() string {
return RobotTable
}
// FromJSON parses robot from json data
func (r *Robot) FromJSON(jsonData string) error {
if len(jsonData) == 0 {
return errors.New("empty json data to parse")
}
return json.Unmarshal([]byte(jsonData), r)
}
// ToJSON marshals Robot to JSON data
func (r *Robot) ToJSON() (string, error) {
data, err := json.Marshal(r)
if err != nil {
return "", err
}
return string(data), nil
}
// RobotQuery ...
type RobotQuery struct {
Name string
ProjectID int64
Disabled bool
FuzzyMatchName bool
Pagination
}
// RobotCreate ...
type RobotCreate struct {
Name string `json:"name"`
ProjectID int64 `json:"pid"`
Description string `json:"description"`
Disabled bool `json:"disabled"`
ExpiresAt int64 `json:"expires_at"`
Visible bool `json:"-"`
Access []*types.Policy `json:"access"`
}
// Pagination ...
type Pagination struct {
Page int64
Size int64
}
// Valid ...
func (rq *RobotCreate) Valid(v *validation.Validation) {
if utils.IsIllegalLength(rq.Name, 1, 255) {
v.SetError("name", "robot name with illegal length")
}
if utils.IsContainIllegalChar(rq.Name, []string{",", "~", "#", "$", "%"}) {
v.SetError("name", "robot name contains illegal characters")
}
if rq.ExpiresAt < -1 {
v.SetError("expires_at", "expiration time must be a positive integer or -1 if set")
}
}
// RobotRep ...
type RobotRep struct {
Name string `json:"name"`
Token string `json:"token"`
}

View File

@ -1,6 +1,8 @@
package model
import (
"encoding/json"
"github.com/goharbor/harbor/src/lib/errors"
"time"
"github.com/astaxie/beego/orm"
@ -28,3 +30,22 @@ type Robot struct {
func (r *Robot) TableName() string {
return "robot"
}
// FromJSON parses robot from json data
func (r *Robot) FromJSON(jsonData string) error {
if len(jsonData) == 0 {
return errors.New("empty json data to parse")
}
return json.Unmarshal([]byte(jsonData), r)
}
// ToJSON marshals Robot to JSON data
func (r *Robot) ToJSON() (string, error) {
data, err := json.Marshal(r)
if err != nil {
return "", err
}
return string(data), nil
}

View File

@ -33,7 +33,7 @@ import (
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/pkg/robot2/model"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
@ -460,7 +460,7 @@ func getInternalTokenServiceEndpoint(ctx job.Context) (string, error) {
// makeBasicAuthorization creates authorization from a robot account based on the arguments for scanning.
func makeBasicAuthorization(robotAccount *model.Robot) (string, error) {
basic := fmt.Sprintf("%s:%s", robotAccount.Name, robotAccount.Token)
basic := fmt.Sprintf("%s:%s", robotAccount.Name, robotAccount.Secret)
encoded := base64.StdEncoding.EncodeToString([]byte(basic))
return fmt.Sprintf("Basic %s", encoded), nil

View File

@ -16,11 +16,12 @@ package scan
import (
"encoding/json"
"github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/pkg/robot2/model"
"testing"
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
@ -90,10 +91,13 @@ func (suite *JobTestSuite) TestJob() {
sData, err := sr.ToJSON()
require.NoError(suite.T(), err)
robot := &model.Robot{
ID: 1,
Name: "robot",
Token: "token",
robot := &robot.Robot{
Robot: model.Robot{
ID: 1,
Name: "robot",
Secret: "token",
},
Level: "project",
}
robotData, err := robot.ToJSON()

View File

@ -21,8 +21,8 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/security"
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
ctl_robot "github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/lib/log"
pkgrobot "github.com/goharbor/harbor/src/pkg/robot"
pkg_token "github.com/goharbor/harbor/src/pkg/token"
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
)
@ -46,8 +46,8 @@ func (r *robot) Generate(req *http.Request) security.Context {
return nil
}
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
ctr := pkgrobot.RobotCtr
robot, err := ctr.GetRobotAccount(rtk.Claims.(*robot_claim.Claim).TokenID)
ctr := ctl_robot.Ctl
robot, err := ctr.Get(req.Context(), rtk.Claims.(*robot_claim.Claim).TokenID, nil)
if err != nil {
log.Errorf("failed to get robot %s: %v", robotName, err)
return nil
@ -65,5 +65,6 @@ func (r *robot) Generate(req *http.Request) security.Context {
return nil
}
log.Debugf("a robot security context generated for request %s %s", req.Method, req.URL.Path)
return robotCtx.NewSecurityContext(robot, rtk.Claims.(*robot_claim.Claim).Access)
return robotCtx.NewSecurityContext(&robot.Robot, false, rtk.Claims.(*robot_claim.Claim).Access)
}

View File

@ -0,0 +1,82 @@
package security
import (
"fmt"
"github.com/goharbor/harbor/src/common/security"
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
"github.com/goharbor/harbor/src/common/utils"
robot_ctl "github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot2/model"
"net/http"
"strings"
)
type robot2 struct{}
func (r *robot2) Generate(req *http.Request) security.Context {
log := log.G(req.Context())
name, secret, ok := req.BasicAuth()
if !ok {
return nil
}
if !strings.HasPrefix(name, config.RobotPrefix()) {
return nil
}
key, err := config.SecretKey()
if err != nil {
log.Error("failed to get secret key")
return nil
}
_, err = utils.ReversibleDecrypt(secret, key)
if err != nil {
log.Errorf("failed to decode secret key: %s, %v", secret, err)
return nil
}
// TODO use the naming pattern to avoid the permission boundary crossing.
robots, err := robot_ctl.Ctl.List(req.Context(), q.New(q.KeyWords{
"name": strings.TrimPrefix(name, config.RobotPrefix()),
}), &robot_ctl.Option{
WithPermission: true,
})
if err != nil {
log.Errorf("failed to list robots: %v", err)
return nil
}
if len(robots) == 0 {
return nil
}
var accesses []*types.Policy
robot := robots[0]
if secret != robot.Secret {
log.Error("the secret provided is not correct.")
return nil
}
if robot.Disabled {
log.Error("the robot is disabled.")
return nil
}
// add the expiration check
for _, p := range robot.Permissions {
for _, a := range p.Access {
accesses = append(accesses, &types.Policy{
Action: a.Action,
Effect: a.Effect,
Resource: types.Resource(fmt.Sprintf("%s/%s", p.Scope, a.Resource)),
})
}
}
modelRobot := &model.Robot{
Name: strings.TrimPrefix(name, config.RobotPrefix()),
}
log.Infof("a robot2 security context generated for request %s %s", req.Method, req.URL.Path)
return robotCtx.NewSecurityContext(modelRobot, robot.Level == robot_ctl.LEVELSYSTEM, accesses)
}

View File

@ -0,0 +1,32 @@
package security
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/utils/test"
core_cfg "github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"os"
"testing"
)
func TestRobot2(t *testing.T) {
secretKeyPath := "/tmp/secretkey"
_, err := test.GenerateKey(secretKeyPath)
assert.Nil(t, err)
defer os.Remove(secretKeyPath)
os.Setenv("KEY_PATH", secretKeyPath)
conf := map[string]interface{}{
common.RobotPrefix: "robot@",
}
core_cfg.InitWithSettings(conf)
robot := &robot2{}
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
require.Nil(t, err)
req.SetBasicAuth("robot@est1", "Harbor12345")
ctx := robot.Generate(req)
assert.Nil(t, ctx)
}

View File

@ -32,6 +32,7 @@ var (
&idToken{},
&authProxy{},
&robot{},
&robot2{},
&basicAuth{},
&session{},
&proxyCacheSecret{},

View File

@ -7,10 +7,12 @@ import (
"github.com/goharbor/harbor/src/server/v2.0/models"
)
// Robot ...
type Robot struct {
*robot.Robot
}
// ToSwagger ...
func (r *Robot) ToSwagger() *models.Robot {
perms := []*models.Permission{}
for _, p := range r.Permissions {

View File

@ -120,7 +120,6 @@ func (rAPI *robotAPI) ListRobot(ctx context.Context, params operation.ListRobotP
} else {
level = robot.LEVELSYSTEM
projectID = 0
query.Keywords["ProjectID"] = 0
}
@ -239,21 +238,13 @@ func (rAPI *robotAPI) validate(r *models.RobotCreate) error {
return errors.New(nil).WithMessage("bad request empty permission").WithCode(errors.BadRequestCode)
}
if r.Level == robot.LEVELPROJECT {
// to create a project robot, the permission must be only one project scope.
if len(r.Permissions) > 1 {
return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode)
}
// to create a project robot, the permission must be only one project scope.
if r.Level == robot.LEVELPROJECT && len(r.Permissions) > 1 {
return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode)
}
return nil
}
func isValidLevel(l string) bool {
switch l {
case
robot.LEVELSYSTEM,
robot.LEVELPROJECT:
return true
}
return false
return l == robot.LEVELSYSTEM || l == robot.LEVELPROJECT
}

View File

@ -0,0 +1,282 @@
package handler
import (
"context"
"fmt"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/permission/types"
pkg_robot "github.com/goharbor/harbor/src/pkg/robot2"
pkg "github.com/goharbor/harbor/src/pkg/robot2/model"
handler_model "github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/robotv1"
"regexp"
"strings"
)
func newRobotV1API() *robotV1API {
return &robotV1API{
robotCtl: robot.Ctl,
robotMgr: pkg_robot.Mgr,
projectCtr: project.Ctl,
}
}
type robotV1API struct {
BaseAPI
robotCtl robot.Controller
robotMgr pkg_robot.Manager
projectCtr project.Controller
}
func (rAPI *robotV1API) CreateRobotV1(ctx context.Context, params operation.CreateRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionCreate, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
if err := rAPI.validate(ctx, params); err != nil {
return rAPI.SendError(ctx, err)
}
r := &robot.Robot{
Robot: pkg.Robot{
Name: params.Robot.Name,
Description: params.Robot.Description,
ExpiresAt: params.Robot.ExpiresAt,
},
Level: robot.LEVELPROJECT,
}
projectID, projectName, err := utils.ParseProjectIDOrName(params.ProjectIDOrName)
if err != nil {
return rAPI.SendError(ctx, err)
}
if projectID != 0 {
p, err := project.Ctl.Get(ctx, projectID)
if err != nil {
log.Errorf("failed to get project %s: %v", projectName, err)
return rAPI.SendError(ctx, err)
}
if p == nil {
log.Warningf("project %s not found", projectName)
return rAPI.SendError(ctx, err)
}
projectName = p.Name
}
permission := &robot.Permission{
Kind: "project",
Namespace: projectName,
}
var policies []*types.Policy
for _, acc := range params.Robot.Access {
policy := &types.Policy{
Action: types.Action(acc.Action),
Effect: types.Effect(acc.Effect),
}
res, err := getRawResource(acc.Resource)
if err != nil {
return rAPI.SendError(ctx, err)
}
policy.Resource = types.Resource(res)
policies = append(policies, policy)
}
permission.Access = policies
r.Permissions = append(r.Permissions, permission)
rid, err := rAPI.robotCtl.Create(ctx, r)
if err != nil {
return rAPI.SendError(ctx, err)
}
created, err := rAPI.robotCtl.Get(ctx, rid, nil)
if err != nil {
return rAPI.SendError(ctx, err)
}
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), created.ID)
return operation.NewCreateRobotV1Created().WithLocation(location).WithPayload(&models.RobotCreated{
ID: created.ID,
Name: created.Name,
Secret: created.Secret,
CreationTime: strfmt.DateTime(created.CreationTime),
})
}
func (rAPI *robotV1API) DeleteRobotV1(ctx context.Context, params operation.DeleteRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionDelete, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
if err != nil {
return rAPI.SendError(ctx, err)
}
r, err := rAPI.robotCtl.List(ctx, q.New(q.KeyWords{"ProjectID": pro.ProjectID, "ID": params.RobotID}), &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
if len(r) == 0 {
return rAPI.SendError(ctx, errors.NotFoundError(fmt.Errorf("cannot find robot with project id: %d and id: %d", pro.ProjectID, params.RobotID)))
}
// ignore the not permissions error.
if err := rAPI.robotCtl.Delete(ctx, params.RobotID); err != nil && errors.IsNotFoundErr(err) {
return rAPI.SendError(ctx, err)
}
return operation.NewDeleteRobotV1OK()
}
func (rAPI *robotV1API) ListRobotV1(ctx context.Context, params operation.ListRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionList, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
query, err := rAPI.BuildQuery(ctx, params.Q, params.Page, params.PageSize)
if err != nil {
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
if err != nil {
return rAPI.SendError(ctx, err)
}
query.Keywords["ProjectID"] = pro.ProjectID
total, err := rAPI.robotCtl.Count(ctx, query)
if err != nil {
return rAPI.SendError(ctx, err)
}
robots, err := rAPI.robotCtl.List(ctx, query, &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
var results []*models.Robot
for _, r := range robots {
results = append(results, handler_model.NewRobot(r).ToSwagger())
}
return operation.NewListRobotV1OK().
WithXTotalCount(total).
WithLink(rAPI.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(results)
}
func (rAPI *robotV1API) GetRobotByIDV1(ctx context.Context, params operation.GetRobotByIDV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionRead, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
if err != nil {
return rAPI.SendError(ctx, err)
}
r, err := rAPI.robotCtl.List(ctx, q.New(q.KeyWords{"ProjectID": pro.ProjectID, "ID": params.RobotID}), &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
if len(r) == 0 {
return rAPI.SendError(ctx, errors.NotFoundError(fmt.Errorf("cannot find robot with project id: %d and id: %d", pro.ProjectID, params.RobotID)))
}
return operation.NewGetRobotByIDV1OK().WithPayload(handler_model.NewRobot(r[0]).ToSwagger())
}
func (rAPI *robotV1API) UpdateRobotV1(ctx context.Context, params operation.UpdateRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionUpdate, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
if err != nil {
return rAPI.SendError(ctx, err)
}
r, err := rAPI.robotCtl.List(ctx, q.New(q.KeyWords{"ProjectID": pro.ProjectID, "ID": params.RobotID}), &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
if len(r) == 0 {
return rAPI.SendError(ctx, errors.NotFoundError(fmt.Errorf("cannot find robot with project id: %d and id: %d", pro.ProjectID, params.RobotID)))
}
robot := r[0]
// for v1 API, only update the name and description.
if robot.Disabled != params.Robot.Disable {
robot.Robot.Disabled = params.Robot.Disable
if err := rAPI.robotMgr.Update(ctx, &robot.Robot); err != nil {
return rAPI.SendError(ctx, err)
}
}
if robot.Description != params.Robot.Description {
robot.Robot.Description = params.Robot.Description
if err := rAPI.robotMgr.Update(ctx, &robot.Robot); err != nil {
return rAPI.SendError(ctx, err)
}
}
return operation.NewUpdateRobotV1OK()
}
func (rAPI *robotV1API) validate(ctx context.Context, params operation.CreateRobotV1Params) error {
if params.Robot == nil {
return errors.New(nil).WithMessage("bad request no robot").WithCode(errors.BadRequestCode)
}
if len(params.Robot.Access) == 0 {
return errors.New(nil).WithMessage("bad request no access").WithCode(errors.BadRequestCode)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
if err != nil {
return err
}
policies := rbac.GetPoliciesOfProject(pro.ProjectID)
mp := map[string]bool{}
for _, policy := range policies {
mp[policy.String()] = true
}
for _, policy := range params.Robot.Access {
p := &types.Policy{}
lib.JSONCopy(p, policy)
if !mp[p.String()] {
return errors.New(nil).WithMessage("%s action of %s resource not exist in project %s", policy.Action, policy.Resource, params.ProjectIDOrName).WithCode(errors.BadRequestCode)
}
}
return nil
}
// /project/1/repository => repository
func getRawResource(resource string) (string, error) {
resourceReg := regexp.MustCompile("^/project/[0-9]+/(?P<repository>[a-z-]+)$")
matches := resourceReg.FindStringSubmatch(resource)
if len(matches) <= 1 {
return "", errors.New(nil).WithMessage("bad resource %s", resource).WithCode(errors.BadRequestCode)
}
return matches[1], nil
}

View File

@ -42,8 +42,6 @@ func registerLegacyRoutes() {
beego.Router("/api/"+version+"/search", &api.SearchAPI{})
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/robots/:id([0-9]+)", &api.RobotAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/"+version+"/quotas", &api.QuotaAPI{}, "get:List")
beego.Router("/api/"+version+"/quotas/:id([0-9]+)", &api.QuotaAPI{}, "get:Get;put:Put")

View File

@ -22,3 +22,4 @@ package controller
//go:generate mockery --case snake --dir ../../controller/scan --name Checker --output ./scan --outpkg scan
//go:generate mockery --case snake --dir ../../controller/scanner --name Controller --output ./scanner --outpkg scanner
//go:generate mockery --case snake --dir ../../controller/replication --name Controller --output ./replication --outpkg replication
//go:generate mockery --case snake --dir ../../controller/robot --name Controller --output ./robot --outpkg robot

View File

@ -0,0 +1,133 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package robot
import (
context "context"
q "github.com/goharbor/harbor/src/lib/q"
mock "github.com/stretchr/testify/mock"
robot "github.com/goharbor/harbor/src/controller/robot"
)
// Controller is an autogenerated mock type for the Controller type
type Controller struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *Controller) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, r
func (_m *Controller) Create(ctx context.Context, r *robot.Robot) (int64, error) {
ret := _m.Called(ctx, r)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *robot.Robot) int64); ok {
r0 = rf(ctx, r)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *robot.Robot) error); ok {
r1 = rf(ctx, r)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, id
func (_m *Controller) Delete(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id, option
func (_m *Controller) Get(ctx context.Context, id int64, option *robot.Option) (*robot.Robot, error) {
ret := _m.Called(ctx, id, option)
var r0 *robot.Robot
if rf, ok := ret.Get(0).(func(context.Context, int64, *robot.Option) *robot.Robot); ok {
r0 = rf(ctx, id, option)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*robot.Robot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, *robot.Option) error); ok {
r1 = rf(ctx, id, option)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query, option
func (_m *Controller) List(ctx context.Context, query *q.Query, option *robot.Option) ([]*robot.Robot, error) {
ret := _m.Called(ctx, query, option)
var r0 []*robot.Robot
if rf, ok := ret.Get(0).(func(context.Context, *q.Query, *robot.Option) []*robot.Robot); ok {
r0 = rf(ctx, query, option)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*robot.Robot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query, *robot.Option) error); ok {
r1 = rf(ctx, query, option)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: ctx, r
func (_m *Controller) Update(ctx context.Context, r *robot.Robot) error {
ret := _m.Called(ctx, r)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *robot.Robot) error); ok {
r0 = rf(ctx, r)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -143,13 +143,13 @@ func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mi
return r0, r1
}
// HandleJobHooks provides a mock function with given fields: trackID, change
func (_m *Controller) HandleJobHooks(trackID string, change *job.StatusChange) error {
ret := _m.Called(trackID, change)
// HandleJobHooks provides a mock function with given fields: ctx, trackID, change
func (_m *Controller) HandleJobHooks(ctx context.Context, trackID string, change *job.StatusChange) error {
ret := _m.Called(ctx, trackID, change)
var r0 error
if rf, ok := ret.Get(0).(func(string, *job.StatusChange) error); ok {
r0 = rf(trackID, change)
if rf, ok := ret.Get(0).(func(context.Context, string, *job.StatusChange) error); ok {
r0 = rf(ctx, trackID, change)
} else {
r0 = ret.Error(0)
}

View File

@ -28,7 +28,6 @@ package pkg
//go:generate mockery --case snake --dir ../../pkg/task --name Manager --output ./task --outpkg task
//go:generate mockery --case snake --dir ../../pkg/task --name ExecutionManager --output ./task --outpkg task
//go:generate mockery --case snake --dir ../../pkg/user --name Manager --output ./user --outpkg user
//go:generate mockery --case snake --dir ../../pkg/robot/dao --name RobotAccountDao --output ./robot/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/rbac --name Manager --output ./rbac --outpkg rbac
//go:generate mockery --case snake --dir ../../pkg/rbac/dao --name DAO --output ./rbac/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/robot2 --name Manager --output ./robot2 --outpkg robot2

View File

@ -1,127 +0,0 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package dao
import (
context "context"
mock "github.com/stretchr/testify/mock"
model "github.com/goharbor/harbor/src/pkg/robot/model"
q "github.com/goharbor/harbor/src/lib/q"
)
// RobotAccountDao is an autogenerated mock type for the RobotAccountDao type
type RobotAccountDao struct {
mock.Mock
}
// CreateRobotAccount provides a mock function with given fields: robot
func (_m *RobotAccountDao) CreateRobotAccount(robot *model.Robot) (int64, error) {
ret := _m.Called(robot)
var r0 int64
if rf, ok := ret.Get(0).(func(*model.Robot) int64); ok {
r0 = rf(robot)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Robot) error); ok {
r1 = rf(robot)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteByProjectID provides a mock function with given fields: ctx, projectID
func (_m *RobotAccountDao) DeleteByProjectID(ctx context.Context, projectID int64) error {
ret := _m.Called(ctx, projectID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, projectID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteRobotAccount provides a mock function with given fields: id
func (_m *RobotAccountDao) DeleteRobotAccount(id int64) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(int64) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetRobotAccount provides a mock function with given fields: id
func (_m *RobotAccountDao) GetRobotAccount(id int64) (*model.Robot, error) {
ret := _m.Called(id)
var r0 *model.Robot
if rf, ok := ret.Get(0).(func(int64) *model.Robot); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Robot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListRobotAccounts provides a mock function with given fields: query
func (_m *RobotAccountDao) ListRobotAccounts(query *q.Query) ([]*model.Robot, error) {
ret := _m.Called(query)
var r0 []*model.Robot
if rf, ok := ret.Get(0).(func(*q.Query) []*model.Robot); ok {
r0 = rf(query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Robot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*q.Query) error); ok {
r1 = rf(query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateRobotAccount provides a mock function with given fields: robot
func (_m *RobotAccountDao) UpdateRobotAccount(robot *model.Robot) error {
ret := _m.Called(robot)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Robot) error); ok {
r0 = rf(robot)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -28,7 +28,7 @@ def get_endpoint():
def _create_client(server, credential, debug, api_type="products"):
cfg = None
if api_type in ('projectv2', 'artifact', 'repository', 'scan', 'preheat', 'replication'):
if api_type in ('projectv2', 'artifact', 'repository', 'scan', 'preheat', 'replication', 'robot'):
cfg = v2_swagger_client.Configuration()
else:
cfg = swagger_client.Configuration()
@ -60,6 +60,7 @@ def _create_client(server, credential, debug, api_type="products"):
"scan": v2_swagger_client.ScanApi(v2_swagger_client.ApiClient(cfg)),
"scanner": swagger_client.ScannersApi(swagger_client.ApiClient(cfg)),
"replication": v2_swagger_client.ReplicationApi(v2_swagger_client.ApiClient(cfg)),
"robot": v2_swagger_client.RobotApi(v2_swagger_client.ApiClient(cfg)),
}.get(api_type,'Error: Wrong API type')
def _assert_status_code(expect_code, return_code):

View File

@ -214,64 +214,6 @@ class Project(base.Base):
base._assert_status_code(expect_status_code, status_code)
return base._get_id_from_header(header)
def add_project_robot_account(self, project_id, project_name, expires_at, robot_name = None, robot_desc = None, has_pull_right = True, has_push_right = True, has_chart_read_right = True, has_chart_create_right = True, expect_status_code = 201, **kwargs):
kwargs['api_type'] = 'products'
if robot_name is None:
robot_name = base._random_name("robot")
if robot_desc is None:
robot_desc = base._random_name("robot_desc")
if has_pull_right is False and has_push_right is False:
has_pull_right = True
access_list = []
resource_by_project_id = "/project/"+str(project_id)+"/repository"
resource_helm_by_project_id = "/project/"+str(project_id)+"/helm-chart"
resource_helm_create_by_project_id = "/project/"+str(project_id)+"/helm-chart-version"
action_pull = "pull"
action_push = "push"
action_read = "read"
action_create = "create"
if has_pull_right is True:
robotAccountAccess = swagger_client.RobotAccountAccess(resource = resource_by_project_id, action = action_pull)
access_list.append(robotAccountAccess)
if has_push_right is True:
robotAccountAccess = swagger_client.RobotAccountAccess(resource = resource_by_project_id, action = action_push)
access_list.append(robotAccountAccess)
if has_chart_read_right is True:
robotAccountAccess = swagger_client.RobotAccountAccess(resource = resource_helm_by_project_id, action = action_read)
access_list.append(robotAccountAccess)
if has_chart_create_right is True:
robotAccountAccess = swagger_client.RobotAccountAccess(resource = resource_helm_create_by_project_id, action = action_create)
access_list.append(robotAccountAccess)
robotAccountCreate = swagger_client.RobotAccountCreate(robot_name, robot_desc, expires_at, access_list)
client = self._get_client(**kwargs)
data = []
data, status_code, header = client.projects_project_id_robots_post_with_http_info(project_id, robotAccountCreate)
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(201, status_code)
return base._get_id_from_header(header), data
def get_project_robot_account_by_id(self, project_id, robot_id, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
data, status_code, _ = client.projects_project_id_robots_robot_id_get_with_http_info(project_id, robot_id)
return data
def disable_project_robot_account(self, project_id, robot_id, disable, expect_status_code = 200, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
robotAccountUpdate = swagger_client.RobotAccountUpdate(disable)
_, status_code, _ = client.projects_project_id_robots_robot_id_put_with_http_info(project_id, robot_id, robotAccountUpdate)
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(200, status_code)
def delete_project_robot_account(self, project_id, robot_id, expect_status_code = 200, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
_, status_code, _ = client.projects_project_id_robots_robot_id_delete_with_http_info(project_id, robot_id)
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(200, status_code)
def query_user_logs(self, project_name, status_code=200, **kwargs):
try:
logs = self.get_project_log(project_name, expect_status_code=status_code, **kwargs)

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
import time
import base
import v2_swagger_client
from v2_swagger_client.rest import ApiException
class Robot(base.Base, object):
def __init__(self):
super(Robot,self).__init__(api_type = "robot")
def create_project_robot(self, project_name, expires_at, robot_name = None, robot_desc = None, has_pull_right = True, has_push_right = True, has_chart_read_right = True, has_chart_create_right = True, expect_status_code = 201, **kwargs):
if robot_name is None:
robot_name = base._random_name("robot")
if robot_desc is None:
robot_desc = base._random_name("robot_desc")
if has_pull_right is False and has_push_right is False:
has_pull_right = True
access_list = []
action_pull = "pull"
action_push = "push"
action_read = "read"
action_create = "create"
if has_pull_right is True:
robotAccountAccess = v2_swagger_client.Access(resource = "repository", action = action_pull)
access_list.append(robotAccountAccess)
if has_push_right is True:
robotAccountAccess = v2_swagger_client.Access(resource = "repository", action = action_push)
access_list.append(robotAccountAccess)
if has_chart_read_right is True:
robotAccountAccess = v2_swagger_client.Access(resource = "helm-chart", action = action_read)
access_list.append(robotAccountAccess)
if has_chart_create_right is True:
robotAccountAccess = v2_swagger_client.Access(resource = "helm-chart-version", action = action_create)
access_list.append(robotAccountAccess)
robotaccountPermissions = v2_swagger_client.Permission(kind = "project", namespace = project_name, access = access_list)
permission_list = []
permission_list.append(robotaccountPermissions)
robotAccountCreate = v2_swagger_client.RobotCreate(name=robot_name, description=robot_desc, expires_at=expires_at, level="project", permissions = permission_list)
client = self._get_client(**kwargs)
data = []
data, status_code, header = client.create_robot_with_http_info(robotAccountCreate)
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(201, status_code)
return base._get_id_from_header(header), data
def get_robot_account_by_id(self, robot_id, **kwargs):
client = self._get_client(**kwargs)
data, status_code, _ = client.get_robot_by_id_with_http_info(robot_id)
return data
def disable_robot_account(self, robot_id, disable, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
data = self.get_robot_account_by_id(robot_id, **kwargs)
robotAccountUpdate = v2_swagger_client.RobotCreate(name=data.name, description=data.description, expires_at=data.expires_at, level=data.level, permissions = data.permissions, disable = disable)
_, status_code, _ = client.update_robot_with_http_info(robot_id, robotAccountUpdate)
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(200, status_code)
def delete_robot_account(self, robot_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
_, status_code, _ = client.delete_robot_with_http_info(robot_id)
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(200, status_code)

View File

@ -8,6 +8,7 @@ from testutils import harbor_server
from testutils import TEARDOWN
import library.repository
import library.helm
from library.robot import Robot
from library.project import Project
from library.user import User
from library.chart import Chart
@ -18,6 +19,7 @@ class TestProjects(unittest.TestCase):
self.project= Project()
self.user= User()
self.chart= Chart()
self.robot = Robot()
self.url = ADMIN_CLIENT["endpoint"]
self.chart_api_url = CHART_API_CLIENT['endpoint']
self.user_push_chart_password = "Aa123456"
@ -56,17 +58,17 @@ class TestProjects(unittest.TestCase):
#3. Create a new robot account(RA) with full priviliges in project(PA) with user(UA);
robot_id, robot_account = self.project.add_project_robot_account(TestProjects.project_id, TestProjects.project_name,
robot_id, robot_account = self.robot.create_project_robot(TestProjects.project_name,
2441000531 ,**TestProjects.USER_CLIENT)
#4. Push chart to project(PA) by Helm2 CLI with robot account(RA);"
library.helm.helm2_add_repo(self.chart_repo_name, "https://"+harbor_server, TestProjects.project_name, robot_account.name, robot_account.token)
library.helm.helm2_push(self.chart_repo_name, self.chart_file, TestProjects.project_name, robot_account.name, robot_account.token)
library.helm.helm2_add_repo(self.chart_repo_name, "https://"+harbor_server, TestProjects.project_name, robot_account.name, robot_account.secret)
library.helm.helm2_push(self.chart_repo_name, self.chart_file, TestProjects.project_name, robot_account.name, robot_account.secret)
#5. Get chart repositry from project(PA) successfully;
self.chart.chart_should_exist(TestProjects.project_name, self.CHART_NAME, **TestProjects.API_CHART_CLIENT)
#6. Push chart to project(PA) by Helm3 CLI with robot account(RA);
chart_cli_ret = library.helm.helm_chart_push_to_harbor(self.chart_file, self.archive, harbor_server, TestProjects.project_name, self.repo_name, self.verion, robot_account.name, robot_account.token)
chart_cli_ret = library.helm.helm_chart_push_to_harbor(self.chart_file, self.archive, harbor_server, TestProjects.project_name, self.repo_name, self.verion, robot_account.name, robot_account.secret)
if __name__ == '__main__':

View File

@ -7,6 +7,7 @@ from testutils import TEARDOWN
from testutils import harbor_server
from library.user import User
from library.project import Project
from library.robot import Robot
from library.repository import Repository
from library.repository import pull_harbor_image
from library.repository import push_image_to_project
@ -18,6 +19,7 @@ class TestProjects(unittest.TestCase):
self.project = Project()
self.user = User()
self.repo = Repository()
self.robot = Robot()
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def tearDown(self):
@ -88,43 +90,43 @@ class TestProjects(unittest.TestCase):
TestProjects.repo_name_in_project_b, tag_b = push_image_to_project(TestProjects.project_ra_name_b, harbor_server, user_ra_name, user_ra_password, image_project_b, tag)
TestProjects.repo_name_in_project_c, tag_c = push_image_to_project(TestProjects.project_ra_name_c, harbor_server, user_ra_name, user_ra_password, image_project_c, tag)
#4. Create a new robot account(RA) with pull and push privilige in project(PA) by user(UA);
robot_id, robot_account = self.project.add_project_robot_account(TestProjects.project_ra_id_a, TestProjects.project_ra_name_a,
#4. Create a new robot account(RA) with pull and push privilege in project(PA) by user(UA);
robot_id, robot_account = self.robot.create_project_robot(TestProjects.project_ra_name_a,
2441000531 ,**TestProjects.USER_RA_CLIENT)
#5. Check robot account info, it should has both pull and push priviliges;
data = self.project.get_project_robot_account_by_id(TestProjects.project_ra_id_a, robot_id, **TestProjects.USER_RA_CLIENT)
#5. Check robot account info, it should has both pull and push privilege;
data = self.robot.get_robot_account_by_id(robot_id, **TestProjects.USER_RA_CLIENT)
_assert_status_code(robot_account.name, data.name)
#6. Pull image(ImagePA) from project(PA) by robot account(RA), it must be successful;
pull_harbor_image(harbor_server, robot_account.name, robot_account.token, TestProjects.repo_name_in_project_a, tag_a)
pull_harbor_image(harbor_server, robot_account.name, robot_account.secret, TestProjects.repo_name_in_project_a, tag_a)
#7. Push image(ImageRA) to project(PA) by robot account(RA), it must be successful;
TestProjects.repo_name_pa, _ = push_image_to_project(TestProjects.project_ra_name_a, harbor_server, robot_account.name, robot_account.token, image_robot_account, tag)
TestProjects.repo_name_pa, _ = push_image_to_project(TestProjects.project_ra_name_a, harbor_server, robot_account.name, robot_account.secret, image_robot_account, tag)
#8. Push image(ImageRA) to project(PB) by robot account(RA), it must be not successful;
push_image_to_project(TestProjects.project_ra_name_b, harbor_server, robot_account.name, robot_account.token, image_robot_account, tag, expected_error_message = "unauthorized to access repository")
push_image_to_project(TestProjects.project_ra_name_b, harbor_server, robot_account.name, robot_account.secret, image_robot_account, tag, expected_error_message = "unauthorized to access repository")
#9. Pull image(ImagePB) from project(PB) by robot account(RA), it must be not successful;
pull_harbor_image(harbor_server, robot_account.name, robot_account.token, TestProjects.repo_name_in_project_b, tag_b, expected_error_message = "unauthorized to access repository")
pull_harbor_image(harbor_server, robot_account.name, robot_account.secret, TestProjects.repo_name_in_project_b, tag_b, expected_error_message = "unauthorized to access repository")
#10. Pull image from project(PC), it must be successful;
pull_harbor_image(harbor_server, robot_account.name, robot_account.token, TestProjects.repo_name_in_project_c, tag_c)
pull_harbor_image(harbor_server, robot_account.name, robot_account.secret, TestProjects.repo_name_in_project_c, tag_c)
#11. Push image(ImageRA) to project(PC) by robot account(RA), it must be not successful;
push_image_to_project(TestProjects.project_ra_name_c, harbor_server, robot_account.name, robot_account.token, image_robot_account, tag, expected_error_message = "unauthorized to access repository")
push_image_to_project(TestProjects.project_ra_name_c, harbor_server, robot_account.name, robot_account.secret, image_robot_account, tag, expected_error_message = "unauthorized to access repository")
#12. Update action property of robot account(RA);"
self.project.disable_project_robot_account(TestProjects.project_ra_id_a, robot_id, True, **TestProjects.USER_RA_CLIENT)
self.robot.disable_robot_account(robot_id, True, **TestProjects.USER_RA_CLIENT)
#13. Pull image(ImagePA) from project(PA) by robot account(RA), it must be not successful;
pull_harbor_image(harbor_server, robot_account.name, robot_account.token, TestProjects.repo_name_in_project_a, tag_a, expected_login_error_message = "unauthorized: authentication required")
pull_harbor_image(harbor_server, robot_account.name, robot_account.secret, TestProjects.repo_name_in_project_a, tag_a, expected_login_error_message = "unauthorized: authentication required")
#14. Push image(ImageRA) to project(PA) by robot account(RA), it must be not successful;
push_image_to_project(TestProjects.project_ra_name_a, harbor_server, robot_account.name, robot_account.token, image_robot_account, tag, expected_login_error_message = "unauthorized: authentication required")
push_image_to_project(TestProjects.project_ra_name_a, harbor_server, robot_account.name, robot_account.secret, image_robot_account, tag, expected_login_error_message = "unauthorized: authentication required")
#15. Delete robot account(RA), it must be not successful.
self.project.delete_project_robot_account(TestProjects.project_ra_id_a, robot_id, **TestProjects.USER_RA_CLIENT)
self.robot.delete_robot_account(robot_id, **TestProjects.USER_RA_CLIENT)
if __name__ == '__main__':
unittest.main()