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' $ref: '#/definitions/NotFoundChartAPIError'
'500': '500':
$ref: '#/definitions/InternalChartAPIError' $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': '/system/oidc/ping':
post: post:
summary: Test the OIDC endpoint. summary: Test the OIDC endpoint.
@ -4633,75 +4452,6 @@ definitions:
error: error:
type: string type: string
description: (optional) The error message when the status is "unhealthy" 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: Permission:
type: object type: object
description: The permission description: The permission

View File

@ -23,12 +23,13 @@ import (
"github.com/goharbor/harbor/src/controller/project" "github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/pkg/permission/evaluator" "github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/types" "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 // SecurityContext implements security.Context interface based on database
type SecurityContext struct { type SecurityContext struct {
robot *model.Robot robot *model.Robot
isSystemLevel bool
ctl project.Controller ctl project.Controller
policy []*types.Policy policy []*types.Policy
evaluator evaluator.Evaluator evaluator evaluator.Evaluator
@ -36,7 +37,7 @@ type SecurityContext struct {
} }
// NewSecurityContext ... // NewSecurityContext ...
func NewSecurityContext(robot *model.Robot, policy []*types.Policy) *SecurityContext { func NewSecurityContext(robot *model.Robot, isSystemLevel bool, policy []*types.Policy) *SecurityContext {
return &SecurityContext{ return &SecurityContext{
ctl: project.Ctl, ctl: project.Ctl,
robot: robot, robot: robot,
@ -76,7 +77,11 @@ func (s *SecurityContext) IsSolutionUser() bool {
// Can returns whether the robot can do action on resource // Can returns whether the robot can do action on resource
func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource types.Resource) bool { func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource types.Resource) bool {
s.once.Do(func() { s.once.Do(func() {
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)) s.evaluator = rbac.NewProjectEvaluator(s.ctl, rbac.NewBuilderForPolicies(s.GetUsername(), s.policy, filterRobotPolicies))
}
}) })
return s.evaluator != nil && s.evaluator.HasPermission(ctx, resource, action) 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/models"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/pkg/permission/types" "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" projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
"github.com/goharbor/harbor/src/testing/mock" "github.com/goharbor/harbor/src/testing/mock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -38,45 +38,45 @@ var (
func TestIsAuthenticated(t *testing.T) { func TestIsAuthenticated(t *testing.T) {
// unauthenticated // unauthenticated
ctx := NewSecurityContext(nil, nil) ctx := NewSecurityContext(nil, false, nil)
assert.False(t, ctx.IsAuthenticated()) assert.False(t, ctx.IsAuthenticated())
// authenticated // authenticated
ctx = NewSecurityContext(&model.Robot{ ctx = NewSecurityContext(&model.Robot{
Name: "test", Name: "test",
Disabled: false, Disabled: false,
}, nil) }, false, nil)
assert.True(t, ctx.IsAuthenticated()) assert.True(t, ctx.IsAuthenticated())
} }
func TestGetUsername(t *testing.T) { func TestGetUsername(t *testing.T) {
// unauthenticated // unauthenticated
ctx := NewSecurityContext(nil, nil) ctx := NewSecurityContext(nil, false, nil)
assert.Equal(t, "", ctx.GetUsername()) assert.Equal(t, "", ctx.GetUsername())
// authenticated // authenticated
ctx = NewSecurityContext(&model.Robot{ ctx = NewSecurityContext(&model.Robot{
Name: "test", Name: "test",
Disabled: false, Disabled: false,
}, nil) }, false, nil)
assert.Equal(t, "test", ctx.GetUsername()) assert.Equal(t, "test", ctx.GetUsername())
} }
func TestIsSysAdmin(t *testing.T) { func TestIsSysAdmin(t *testing.T) {
// unauthenticated // unauthenticated
ctx := NewSecurityContext(nil, nil) ctx := NewSecurityContext(nil, false, nil)
assert.False(t, ctx.IsSysAdmin()) assert.False(t, ctx.IsSysAdmin())
// authenticated, non admin // authenticated, non admin
ctx = NewSecurityContext(&model.Robot{ ctx = NewSecurityContext(&model.Robot{
Name: "test", Name: "test",
Disabled: false, Disabled: false,
}, nil) }, false, nil)
assert.False(t, ctx.IsSysAdmin()) assert.False(t, ctx.IsSysAdmin())
} }
func TestIsSolutionUser(t *testing.T) { func TestIsSolutionUser(t *testing.T) {
ctx := NewSecurityContext(nil, nil) ctx := NewSecurityContext(nil, false, nil)
assert.False(t, ctx.IsSolutionUser()) assert.False(t, ctx.IsSolutionUser())
} }
@ -95,7 +95,7 @@ func TestHasPullPerm(t *testing.T) {
ctl := &projecttesting.Controller{} ctl := &projecttesting.Controller{}
mock.OnAnything(ctl, "Get").Return(private, nil) mock.OnAnything(ctl, "Get").Return(private, nil)
ctx := NewSecurityContext(robot, policies) ctx := NewSecurityContext(robot, false, policies)
ctx.ctl = ctl ctx.ctl = ctl
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository) resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPull, resource)) assert.True(t, ctx.Can(context.TODO(), rbac.ActionPull, resource))
@ -116,7 +116,7 @@ func TestHasPushPerm(t *testing.T) {
ctl := &projecttesting.Controller{} ctl := &projecttesting.Controller{}
mock.OnAnything(ctl, "Get").Return(private, nil) mock.OnAnything(ctl, "Get").Return(private, nil)
ctx := NewSecurityContext(robot, policies) ctx := NewSecurityContext(robot, false, policies)
ctx.ctl = ctl ctx.ctl = ctl
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository) resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPush, resource)) assert.True(t, ctx.Can(context.TODO(), rbac.ActionPush, resource))
@ -141,7 +141,7 @@ func TestHasPushPullPerm(t *testing.T) {
ctl := &projecttesting.Controller{} ctl := &projecttesting.Controller{}
mock.OnAnything(ctl, "Get").Return(private, nil) mock.OnAnything(ctl, "Get").Return(private, nil)
ctx := NewSecurityContext(robot, policies) ctx := NewSecurityContext(robot, false, policies)
ctx.ctl = ctl ctx.ctl = ctl
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository) 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)) 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 { if err != nil {
return 0, err return 0, err
} }
r.ID = robotID
if err := d.createPermission(ctx, r); err != nil { if err := d.createPermission(ctx, r); err != nil {
return 0, err 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) log.Errorf("failed to decode scope of robot %d: %v", r.ID, err)
return err return err
} }
p.Scope = scope
p.Kind = kind p.Kind = kind
p.Namespace = namespace p.Namespace = namespace
p.Access = accesses p.Access = accesses

View File

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

View File

@ -18,20 +18,21 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/goharbor/harbor/src/controller/project"
"sync" "sync"
cj "github.com/goharbor/harbor/src/common/job" cj "github.com/goharbor/harbor/src/common/job"
jm "github.com/goharbor/harbor/src/common/job/models" jm "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
ar "github.com/goharbor/harbor/src/controller/artifact" ar "github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/robot"
sc "github.com/goharbor/harbor/src/controller/scanner" sc "github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot" "github.com/goharbor/harbor/src/pkg/robot2/model"
"github.com/goharbor/harbor/src/pkg/robot/model"
sca "github.com/goharbor/harbor/src/pkg/scan" sca "github.com/goharbor/harbor/src/pkg/scan"
"github.com/goharbor/harbor/src/pkg/scan/all" "github.com/goharbor/harbor/src/pkg/scan/all"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan" "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
@ -70,6 +71,8 @@ type basicController struct {
sc sc.Controller sc sc.Controller
// Robot account controller // Robot account controller
rc robot.Controller rc robot.Controller
// Project controller
pro project.Controller
// Job service client // Job service client
jc jcGetter jc jcGetter
// UUID generator // UUID generator
@ -88,7 +91,9 @@ func NewController() Controller {
// Refer to the default scanner controller // Refer to the default scanner controller
sc: sc.DefaultController, sc: sc.DefaultController,
// Refer to the default robot account controller // 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 // Refer to the default job service client
jc: func() cj.Client { jc: func() cj.Client {
return cj.GlobalClient 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 { 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 { if err != nil {
// Update the status to the concrete error // Update the status to the concrete error
// Change status code to normal error code // Change status code to normal error code
@ -494,7 +499,7 @@ func (bc *basicController) GetScanLog(uuid string) ([]byte, error) {
} }
// HandleJobHooks ... // 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 { if len(trackID) == 0 {
return errors.New("empty track ID") return errors.New("empty track ID")
} }
@ -514,7 +519,7 @@ func (bc *basicController) HandleJobHooks(trackID string, change *job.StatusChan
} }
if r.ID > 0 { 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 // Should not block the main flow, just logged
log.Error(errors.Wrap(err, "scan controller: handle job hook")) log.Error(errors.Wrap(err, "scan controller: handle job hook"))
} else { } 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. // 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. // Use uuid as name to avoid duplicated entries.
UUID, err := bc.uuid() UUID, err := bc.uuid()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "scan controller: make robot account") return nil, errors.Wrap(err, "scan controller: make robot account")
} }
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository) p, err := bc.pro.Get(ctx, projectID)
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)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "scan controller: make robot account") 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 // 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 var ck string
if registration.UseInternalAddr { if registration.UseInternalAddr {
ck = configCoreInternalAddr ck = configCoreInternalAddr
@ -618,7 +648,7 @@ func (bc *basicController) launchScanJob(trackID string, artifact *ar.Artifact,
return "", errors.Wrap(err, "scan controller: launch scan job") 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 { if err != nil {
return "", errors.Wrap(err, "scan controller: launch scan job") return "", errors.Wrap(err, "scan controller: launch scan job")
} }

View File

@ -19,6 +19,8 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
models "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/robot"
"testing" "testing"
"time" "time"
@ -27,15 +29,16 @@ import (
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/artifact" "github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/jobservice/job" "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/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" 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/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner" "github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln" "github.com/goharbor/harbor/src/pkg/scan/vuln"
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact" 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" scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
mocktesting "github.com/goharbor/harbor/src/testing/mock" mocktesting "github.com/goharbor/harbor/src/testing/mock"
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report" 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) mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
suite.reportMgr = mgr suite.reportMgr = mgr
rc := &MockRobotController{} rc := &robottesting.Controller{}
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},
}
rname := fmt.Sprintf("%s-%s", suite.registration.Name, "the-uuid-123") rname := fmt.Sprintf("%s-%s", suite.registration.Name, "the-uuid-123")
account := &model.RobotCreate{
account := &robot.Robot{
Robot: model.Robot{
Name: rname, Name: rname,
Description: "for scan", Description: "for scan",
ProjectID: suite.artifact.ProjectID, ProjectID: suite.artifact.ProjectID,
Access: access, 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{
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, ID: 1,
Name: rname, Name: rname,
Token: "robot-account", Secret: "robot-account",
Description: "for scan", Description: "for scan",
ProjectID: suite.artifact.ProjectID, ProjectID: suite.artifact.ProjectID,
},
Level: "project",
}, nil) }, nil)
// Set job parameters // Set job parameters
@ -208,7 +232,8 @@ func (suite *ControllerTestSuite) SetupSuite() {
regJSON, err := suite.registration.ToJSON() regJSON, err := suite.registration.ToJSON()
require.NoError(suite.T(), err) 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() robotJSON, err := rb.ToJSON()
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
@ -237,6 +262,12 @@ func (suite *ControllerTestSuite) SetupSuite() {
walkFn(suite.artifact) 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{ suite.c = &basicController{
manager: mgr, manager: mgr,
ar: suite.ar, ar: suite.ar,
@ -245,6 +276,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
return jc return jc
}, },
rc: rc, rc: rc,
pro: proCtl,
uuid: func() (string, error) { uuid: func() (string, error) {
return "the-uuid-123", nil 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) 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) 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: // Returns:
// error : non nil error if any errors occurred // 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 // 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/CVEAllowlist", &SysCVEAllowlistAPI{}, "get:Get;put:Put")
beego.Router("/api/system/oidc/ping", &OIDCAPI{}, "post:Ping") 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/adapters", &ReplicationAdapterAPI{}, "get:List")
beego.Router("/api/replication/policies", &ReplicationPolicyAPI{}, "get:List;post:Create") 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 ( import (
"encoding/json" "encoding/json"
"github.com/goharbor/harbor/src/lib/orm"
"time" "time"
"github.com/goharbor/harbor/src/common/job" "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") err = errors.Wrap(err, "scan job hook handler")
log.Error(err) log.Error(err)
h.SendInternalServerError(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 package model
import ( import (
"encoding/json"
"github.com/goharbor/harbor/src/lib/errors"
"time" "time"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
@ -28,3 +30,22 @@ type Robot struct {
func (r *Robot) TableName() string { func (r *Robot) TableName() string {
return "robot" 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/job"
"github.com/goharbor/harbor/src/jobservice/logger" "github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/errors" "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/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/report" "github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" 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. // makeBasicAuthorization creates authorization from a robot account based on the arguments for scanning.
func makeBasicAuthorization(robotAccount *model.Robot) (string, error) { 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)) encoded := base64.StdEncoding.EncodeToString([]byte(basic))
return fmt.Sprintf("Basic %s", encoded), nil return fmt.Sprintf("Basic %s", encoded), nil

View File

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

View File

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

View File

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

View File

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

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+"/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/?:name", &api.MetadataAPI{}, "get:Get")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post") 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", &api.QuotaAPI{}, "get:List")
beego.Router("/api/"+version+"/quotas/:id([0-9]+)", &api.QuotaAPI{}, "get:Get;put:Put") 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/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/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/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 return r0, r1
} }
// HandleJobHooks provides a mock function with given fields: trackID, change // HandleJobHooks provides a mock function with given fields: ctx, trackID, change
func (_m *Controller) HandleJobHooks(trackID string, change *job.StatusChange) error { func (_m *Controller) HandleJobHooks(ctx context.Context, trackID string, change *job.StatusChange) error {
ret := _m.Called(trackID, change) ret := _m.Called(ctx, trackID, change)
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(string, *job.StatusChange) error); ok { if rf, ok := ret.Get(0).(func(context.Context, string, *job.StatusChange) error); ok {
r0 = rf(trackID, change) r0 = rf(ctx, trackID, change)
} else { } else {
r0 = ret.Error(0) 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 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/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/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 --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/rbac/dao --name DAO --output ./rbac/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/robot2 --name Manager --output ./robot2 --outpkg robot2 //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"): def _create_client(server, credential, debug, api_type="products"):
cfg = None 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() cfg = v2_swagger_client.Configuration()
else: else:
cfg = swagger_client.Configuration() 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)), "scan": v2_swagger_client.ScanApi(v2_swagger_client.ApiClient(cfg)),
"scanner": swagger_client.ScannersApi(swagger_client.ApiClient(cfg)), "scanner": swagger_client.ScannersApi(swagger_client.ApiClient(cfg)),
"replication": v2_swagger_client.ReplicationApi(v2_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') }.get(api_type,'Error: Wrong API type')
def _assert_status_code(expect_code, return_code): 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) base._assert_status_code(expect_status_code, status_code)
return base._get_id_from_header(header) 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): def query_user_logs(self, project_name, status_code=200, **kwargs):
try: try:
logs = self.get_project_log(project_name, expect_status_code=status_code, **kwargs) 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 from testutils import TEARDOWN
import library.repository import library.repository
import library.helm import library.helm
from library.robot import Robot
from library.project import Project from library.project import Project
from library.user import User from library.user import User
from library.chart import Chart from library.chart import Chart
@ -18,6 +19,7 @@ class TestProjects(unittest.TestCase):
self.project= Project() self.project= Project()
self.user= User() self.user= User()
self.chart= Chart() self.chart= Chart()
self.robot = Robot()
self.url = ADMIN_CLIENT["endpoint"] self.url = ADMIN_CLIENT["endpoint"]
self.chart_api_url = CHART_API_CLIENT['endpoint'] self.chart_api_url = CHART_API_CLIENT['endpoint']
self.user_push_chart_password = "Aa123456" 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); #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) 2441000531 ,**TestProjects.USER_CLIENT)
#4. Push chart to project(PA) by Helm2 CLI with robot account(RA);" #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_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.token) 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; #5. Get chart repositry from project(PA) successfully;
self.chart.chart_should_exist(TestProjects.project_name, self.CHART_NAME, **TestProjects.API_CHART_CLIENT) 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); #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__': if __name__ == '__main__':

View File

@ -7,6 +7,7 @@ from testutils import TEARDOWN
from testutils import harbor_server from testutils import harbor_server
from library.user import User from library.user import User
from library.project import Project from library.project import Project
from library.robot import Robot
from library.repository import Repository from library.repository import Repository
from library.repository import pull_harbor_image from library.repository import pull_harbor_image
from library.repository import push_image_to_project from library.repository import push_image_to_project
@ -18,6 +19,7 @@ class TestProjects(unittest.TestCase):
self.project = Project() self.project = Project()
self.user = User() self.user = User()
self.repo = Repository() self.repo = Repository()
self.robot = Robot()
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.") @unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def tearDown(self): 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_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) 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); #4. Create a new robot account(RA) with pull and push privilege 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, robot_id, robot_account = self.robot.create_project_robot(TestProjects.project_ra_name_a,
2441000531 ,**TestProjects.USER_RA_CLIENT) 2441000531 ,**TestProjects.USER_RA_CLIENT)
#5. Check robot account info, it should has both pull and push priviliges; #5. Check robot account info, it should has both pull and push privilege;
data = self.project.get_project_robot_account_by_id(TestProjects.project_ra_id_a, robot_id, **TestProjects.USER_RA_CLIENT) data = self.robot.get_robot_account_by_id(robot_id, **TestProjects.USER_RA_CLIENT)
_assert_status_code(robot_account.name, data.name) _assert_status_code(robot_account.name, data.name)
#6. Pull image(ImagePA) from project(PA) by robot account(RA), it must be successful; #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; #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; #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; #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; #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; #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);" #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; #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; #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. #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__': if __name__ == '__main__':
unittest.main() unittest.main()