Commit a86afd6e authored by Steven Zou's avatar Steven Zou
Browse files

Merge branch 'master' into feature/pluggable_scanner_s3_merge

parents 9fd8b630 21a0dd1d
......@@ -959,13 +959,13 @@ paths:
description: User ID does not exist.
'500':
description: Unexpected internal errors.
'/users/{user_id}/gen_cli_secret':
post:
summary: Generate new CLI secret for a user.
'/users/{user_id}/cli_secret':
put:
summary: Set CLI secret for a user.
description: |
This endpoint let user generate a new CLI secret for himself. This API only works when auth mode is set to 'OIDC'.
Once this API returns with successful status, the old secret will be invalid, as there will be only one CLI secret
for a user. The new secret will be returned in the response.
for a user.
parameters:
- name: user_id
in: path
......@@ -973,19 +973,23 @@ paths:
format: int
required: true
description: User ID
tags:
- Products
responses:
'200':
description: The secret is successfully generated.
- name: input_secret
in: body
description: JSON object that includes the new secret
required: true
schema:
type: object
properties:
secret:
type: string
description: The new secret
tags:
- Products
responses:
'200':
description: The secret is successfully updated
'400':
description: Invalid user ID. Or user is not onboarded via OIDC authentication.
description: Invalid user ID. Or user is not onboarded via OIDC authentication. Or the secret does not meet the standard.
'401':
description: User need to log in first.
'403':
......
......@@ -906,7 +906,7 @@ For example, you have following tags, listed according to their push time, and a
You configure a retention policy to retain the two latest tags that match `harbor-*`, so that `harbor-rc` and `harbor-latest` are deleted. However, since all tags refer to the same SHA digest, this policy would also delete the tags `harbor-1.8` and `harbor-release`, so all tags are retained.
### Combining Rules on a Respository
### Combining Rules on a Repository
You can define up to 15 rules per project. You can apply multiple rules to a repository or set of repositories. When you apply multiple rules to a repository, they are applied with `OR` logic rather than with `AND` logic. In this way, there is no prioritization of application of the rules on a given repository. Rules run concurrently in the background, and the resulting sets from each rule are combined at the end of the run.
......
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
"strings"
"time"
)
// AddRobot ...
func AddRobot(robot *models.Robot) (int64, error) {
now := time.Now()
robot.CreationTime = now
robot.UpdateTime = now
id, err := GetOrmer().Insert(robot)
if err != nil {
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return 0, ErrDupRows
}
return 0, err
}
return id, nil
}
// GetRobotByID ...
func GetRobotByID(id int64) (*models.Robot, error) {
robot := &models.Robot{
ID: id,
}
if err := GetOrmer().Read(robot); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return robot, nil
}
// ListRobots list robots according to the query conditions
func ListRobots(query *models.RobotQuery) ([]*models.Robot, error) {
qs := getRobotQuerySetter(query).OrderBy("Name")
if query != nil {
if query.Size > 0 {
qs = qs.Limit(query.Size)
if query.Page > 0 {
qs = qs.Offset((query.Page - 1) * query.Size)
}
}
}
robots := []*models.Robot{}
_, err := qs.All(&robots)
return robots, err
}
func getRobotQuerySetter(query *models.RobotQuery) orm.QuerySeter {
qs := GetOrmer().QueryTable(&models.Robot{})
if query == nil {
return qs
}
if len(query.Name) > 0 {
if query.FuzzyMatchName {
qs = qs.Filter("Name__icontains", query.Name)
} else {
qs = qs.Filter("Name", query.Name)
}
}
if query.ProjectID != 0 {
qs = qs.Filter("ProjectID", query.ProjectID)
}
return qs
}
// CountRobot ...
func CountRobot(query *models.RobotQuery) (int64, error) {
return getRobotQuerySetter(query).Count()
}
// UpdateRobot ...
func UpdateRobot(robot *models.Robot) error {
robot.UpdateTime = time.Now()
_, err := GetOrmer().Update(robot)
return err
}
// DeleteRobot ...
func DeleteRobot(id int64) error {
_, err := GetOrmer().QueryTable(&models.Robot{}).Filter("ID", id).Delete()
return err
}
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAddRobot(t *testing.T) {
robotName := "test1"
robot := &models.Robot{
Name: robotName,
Description: "test1 description",
ProjectID: 1,
}
// add
id, err := AddRobot(robot)
require.Nil(t, err)
robot.ID = id
require.Nil(t, err)
assert.NotNil(t, id)
}
func TestGetRobot(t *testing.T) {
robotName := "test2"
robot := &models.Robot{
Name: robotName,
Description: "test2 description",
ProjectID: 1,
}
// add
id, err := AddRobot(robot)
require.Nil(t, err)
robot.ID = id
robot, err = GetRobotByID(id)
require.Nil(t, err)
assert.Equal(t, robotName, robot.Name)
}
func TestListRobots(t *testing.T) {
robotName := "test3"
robot := &models.Robot{
Name: robotName,
Description: "test3 description",
ProjectID: 1,
}
_, err := AddRobot(robot)
require.Nil(t, err)
robots, err := ListRobots(&models.RobotQuery{
ProjectID: 1,
})
require.Nil(t, err)
assert.Equal(t, 3, len(robots))
}
func TestDisableRobot(t *testing.T) {
robotName := "test4"
robot := &models.Robot{
Name: robotName,
Description: "test4 description",
ProjectID: 1,
}
// add
id, err := AddRobot(robot)
require.Nil(t, err)
// Disable
robot.Disabled = true
err = UpdateRobot(robot)
require.Nil(t, err)
// Get
robot, err = GetRobotByID(id)
require.Nil(t, err)
assert.Equal(t, true, robot.Disabled)
}
func TestEnableRobot(t *testing.T) {
robotName := "test5"
robot := &models.Robot{
Name: robotName,
Description: "test5 description",
Disabled: true,
ProjectID: 1,
}
// add
id, err := AddRobot(robot)
require.Nil(t, err)
// Disable
robot.Disabled = false
err = UpdateRobot(robot)
require.Nil(t, err)
// Get
robot, err = GetRobotByID(id)
require.Nil(t, err)
assert.Equal(t, false, robot.Disabled)
}
func TestDeleteRobot(t *testing.T) {
robotName := "test6"
robot := &models.Robot{
Name: robotName,
Description: "test6 description",
ProjectID: 1,
}
// add
id, err := AddRobot(robot)
require.Nil(t, err)
// Disable
err = DeleteRobot(id)
require.Nil(t, err)
// Get
robot, err = GetRobotByID(id)
assert.Nil(t, robot)
}
func TestListAllRobot(t *testing.T) {
robots, err := ListRobots(nil)
require.Nil(t, err)
assert.Equal(t, 5, len(robots))
}
......@@ -35,7 +35,6 @@ func init() {
new(UserGroup),
new(AdminJob),
new(JobLog),
new(Robot),
new(OIDCUser),
new(NotificationPolicy),
new(NotificationJob),
......
......@@ -18,17 +18,18 @@ import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
// SecurityContext implements security.Context interface based on database
type SecurityContext struct {
robot *models.Robot
robot *model.Robot
pm promgr.ProjectManager
policy []*rbac.Policy
}
// NewSecurityContext ...
func NewSecurityContext(robot *models.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext {
func NewSecurityContext(robot *model.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext {
return &SecurityContext{
robot: robot,
pm: pm,
......
......@@ -26,6 +26,7 @@ import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
......@@ -96,7 +97,7 @@ func TestIsAuthenticated(t *testing.T) {
assert.False(t, ctx.IsAuthenticated())
// authenticated
ctx = NewSecurityContext(&models.Robot{
ctx = NewSecurityContext(&model.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
......@@ -109,7 +110,7 @@ func TestGetUsername(t *testing.T) {
assert.Equal(t, "", ctx.GetUsername())
// authenticated
ctx = NewSecurityContext(&models.Robot{
ctx = NewSecurityContext(&model.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
......@@ -122,7 +123,7 @@ func TestIsSysAdmin(t *testing.T) {
assert.False(t, ctx.IsSysAdmin())
// authenticated, non admin
ctx = NewSecurityContext(&models.Robot{
ctx = NewSecurityContext(&model.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
......@@ -141,7 +142,7 @@ func TestHasPullPerm(t *testing.T) {
Action: rbac.ActionPull,
},
}
robot := &models.Robot{
robot := &model.Robot{
Name: "test_robot_1",
Description: "desc",
}
......@@ -158,7 +159,7 @@ func TestHasPushPerm(t *testing.T) {
Action: rbac.ActionPush,
},
}
robot := &models.Robot{
robot := &model.Robot{
Name: "test_robot_2",
Description: "desc",
}
......@@ -179,7 +180,7 @@ func TestHasPushPullPerm(t *testing.T) {
Action: rbac.ActionPull,
},
}
robot := &models.Robot{
robot := &model.Robot{
Name: "test_robot_3",
Description: "desc",
}
......
......@@ -97,9 +97,10 @@ func TestImmutableTagRuleAPI_List(t *testing.T) {
func TestImmutableTagRuleAPI_Post(t *testing.T) {
// body := `{
// "id":0,
// "projectID":1,
// "projectID":1,
// "priority":0,
// "template": "immutable_template",
// "action": "immutable",
// "disabled":false,
// "action":"immutable",
// "template":"immutable_template",
......
......@@ -15,30 +15,29 @@
package api
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/token"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/robot"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/pkg/errors"
"net/http"
"strconv"
)
// RobotAPI ...
type RobotAPI struct {
BaseController
project *models.Project
robot *models.Robot
ctr robot.Controller
robot *model.Robot
}
// Prepare ...
func (r *RobotAPI) Prepare() {
r.BaseController.Prepare()
method := r.Ctx.Request.Method
......@@ -68,6 +67,7 @@ func (r *RobotAPI) Prepare() {
return
}
r.project = project
r.ctr = robot.RobotCtr
if method == http.MethodPut || method == http.MethodDelete {
id, err := r.GetInt64FromPath(":id")
......@@ -75,8 +75,7 @@ func (r *RobotAPI) Prepare() {
r.SendBadRequestError(errors.New("invalid robot ID"))
return
}
robot, err := dao.GetRobotByID(id)
robot, err := r.ctr.GetRobotAccount(id)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to get robot %d: %v", id, err))
return
......@@ -101,7 +100,7 @@ func (r *RobotAPI) Post() {
return
}
var robotReq models.RobotReq
var robotReq model.RobotCreate
isValid, err := r.DecodeJSONReqAndValidate(&robotReq)
if !isValid {
r.SendBadRequestError(err)
......@@ -113,59 +112,25 @@ func (r *RobotAPI) Post() {
return
}
// Token duration in minutes
tokenDuration := time.Duration(config.RobotTokenDuration()) * time.Minute
expiresAt := time.Now().UTC().Add(tokenDuration).Unix()
createdName := common.RobotPrefix + robotReq.Name
// first to add a robot account, and get its id.
robot := models.Robot{
Name: createdName,
Description: robotReq.Description,
ProjectID: r.project.ProjectID,
ExpiresAt: expiresAt,
}
id, err := dao.AddRobot(&robot)
robot, err := r.ctr.CreateRobotAccount(&robotReq)
if err != nil {
if err == dao.ErrDupRows {
r.SendConflictError(errors.New("conflict robot account"))
return
}
r.SendInternalServerError(fmt.Errorf("failed to create robot account: %v", err))
r.SendInternalServerError(errors.Wrap(err, "robot API: post"))
return
}
// generate the token, and return it with response data.
// token is not stored in the database.
jwtToken, err := token.New(id, r.project.ProjectID, expiresAt, robotReq.Access)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to valid parameters to generate token for robot account, %v", err))
err := dao.DeleteRobot(id)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to delete the robot account: %d, %v", id, err))
}
return
}
rawTk, err := jwtToken.Raw()
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to sign token for robot account, %v", err))
err := dao.DeleteRobot(id)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to delete the robot account: %d, %v", id, err))
}
return
}
w := r.Ctx.ResponseWriter
w.Header().Set("Content-Type", "application/json")
robotRep := models.RobotRep{
robotRep := model.RobotRep{
Name: robot.Name,
Token: rawTk,
Token: robot.Token,
}
w := r.Ctx.ResponseWriter
w.Header().Set("Content-Type", "application/json")
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
r.Redirect(http.StatusCreated, strconv.FormatInt(robot.ID, 10))
r.Data["json"] = robotRep
r.ServeJSON()
}
......@@ -176,28 +141,19 @@ func (r *RobotAPI) List() {
return
}
query := models.RobotQuery{
ProjectID: r.project.ProjectID,
}
count, err := dao.CountRobot(&query)
robots, err := r.ctr.ListRobotAccount(r.project.ProjectID)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to list robots on project: %d, %v", r.project.ProjectID, err))
r.SendInternalServerError(errors.Wrap(err, "robot API: list"))
return
}
query.Page, query.Size, err = r.GetPaginationParams()
count := len(robots)
page, size, err := r.GetPaginationParams()
if err != nil {
r.SendBadRequestError(err)
return
}
robots, err := dao.ListRobots(&query)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to get robots %v", err))
return
}
r.SetPaginationHeader(count, query.Page, query.Size)
r.SetPaginationHeader(int64(count), page, size)
r.Data["json"] = robots
r.ServeJSON()
}
......@@ -214,13 +170,13 @@ func (r *RobotAPI) Get() {
return
}
robot, err := dao.GetRobotByID(id)
robot, err := r.ctr.GetRobotAccount(id)
if err != nil {
r.SendInternalServerError(fmt.Errorf("failed to get robot %d: %v", id, err))
r.SendInternalServerError(errors.Wrap(err, "robot API: get robot"))
return
}
if robot == nil {
r.SendNotFoundError(fmt.Errorf("robot %d not found", id))
r.SendNotFoundError(fmt.Errorf("robot API: robot %d not found", id))
return
}
......@@ -234,7 +190,7 @@ func (r *RobotAPI) Put() {
return
}
var robotReq models.RobotReq
var robotReq model.RobotCreate
if err := r.DecodeJSONReq(&robotReq); err != nil {
r.SendBadRequestError(err)