diff --git a/api/harbor/swagger.yaml b/api/harbor/swagger.yaml index 2f94af36b..0fc178ee6 100644 --- a/api/harbor/swagger.yaml +++ b/api/harbor/swagger.yaml @@ -6239,6 +6239,9 @@ definitions: 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 diff --git a/src/core/api/robot_test.go b/src/core/api/robot_test.go index 05861ea2d..e8ac014be 100644 --- a/src/core/api/robot_test.go +++ b/src/core/api/robot_test.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" @@ -40,6 +41,9 @@ func TestRobotAPIPost(t *testing.T) { policies := []*rbac.Policy{} policies = append(policies, rbacPolicy) + tokenDuration := time.Duration(30) * time.Minute + expiresAt := time.Now().UTC().Add(tokenDuration).Unix() + cases := []*codeCheckingCase{ // 401 { @@ -68,6 +72,7 @@ func TestRobotAPIPost(t *testing.T) { bodyJSON: &model.RobotCreate{ Name: "test", Description: "test desc", + ExpiresAt: expiresAt, Access: policies, }, credential: projAdmin4Robot, @@ -82,6 +87,20 @@ func TestRobotAPIPost(t *testing.T) { 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, }, @@ -94,6 +113,7 @@ func TestRobotAPIPost(t *testing.T) { bodyJSON: &model.RobotCreate{ Name: "test", Description: "resource not exist", + ExpiresAt: expiresAt, Access: []*rbac.Policy{ {Resource: res.Subresource("foo"), Action: rbac.ActionCreate}, }, @@ -109,6 +129,7 @@ func TestRobotAPIPost(t *testing.T) { bodyJSON: &model.RobotCreate{ Name: "test", Description: "action not exist", + ExpiresAt: expiresAt, Access: []*rbac.Policy{ {Resource: res.Subresource(rbac.ResourceRepository), Action: "foo"}, }, @@ -124,6 +145,7 @@ func TestRobotAPIPost(t *testing.T) { bodyJSON: &model.RobotCreate{ Name: "test", Description: "policy not exit", + ExpiresAt: expiresAt, Access: []*rbac.Policy{ {Resource: res.Subresource(rbac.ResourceMember), Action: rbac.ActionPush}, }, @@ -140,6 +162,7 @@ func TestRobotAPIPost(t *testing.T) { bodyJSON: &model.RobotCreate{ Name: "test2", Description: "test2 desc", + ExpiresAt: expiresAt, }, credential: projDeveloper, }, @@ -154,6 +177,7 @@ func TestRobotAPIPost(t *testing.T) { bodyJSON: &model.RobotCreate{ Name: "test", Description: "test desc", + ExpiresAt: expiresAt, Access: policies, }, credential: projAdmin4Robot, diff --git a/src/pkg/robot/controller.go b/src/pkg/robot/controller.go index 1d5acc170..7aa153769 100644 --- a/src/pkg/robot/controller.go +++ b/src/pkg/robot/controller.go @@ -58,17 +58,19 @@ func (d *DefaultAPIController) GetRobotAccount(id int64) (*model.Robot, error) { func (d *DefaultAPIController) CreateRobotAccount(robotReq *model.RobotCreate) (*model.Robot, error) { var deferDel error - // Token duration in minutes - tokenDuration := time.Duration(config.RobotTokenDuration()) * time.Minute - expiresAt := time.Now().UTC().Add(tokenDuration).Unix() 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: expiresAt, + ExpiresAt: robotReq.ExpiresAt, Visible: robotReq.Visible, } id, err := d.manager.CreateRobotAccount(robot) @@ -85,7 +87,7 @@ func (d *DefaultAPIController) CreateRobotAccount(robotReq *model.RobotCreate) ( Access: robotReq.Access, StandardClaims: jwt.StandardClaims{ IssuedAt: time.Now().UTC().Unix(), - ExpiresAt: expiresAt, + ExpiresAt: robotReq.ExpiresAt, Issuer: opt.Issuer, }, } diff --git a/src/pkg/robot/controller_test.go b/src/pkg/robot/controller_test.go index 6b6fdd70a..6cea12a96 100644 --- a/src/pkg/robot/controller_test.go +++ b/src/pkg/robot/controller_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "testing" + "time" ) type ControllerTestSuite struct { @@ -47,9 +48,13 @@ func (s *ControllerTestSuite) TestRobotAccount() { policies := []*rbac.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, } @@ -74,6 +79,7 @@ func (s *ControllerTestSuite) TestRobotAccount() { robot2 := &model.RobotCreate{ Name: "robot2", Description: "TestCreateRobotAccount", + ExpiresAt: expiresAt, ProjectID: int64(1), Access: policies, } diff --git a/src/pkg/robot/model/robot.go b/src/pkg/robot/model/robot.go index db911f11c..578659388 100644 --- a/src/pkg/robot/model/robot.go +++ b/src/pkg/robot/model/robot.go @@ -49,6 +49,7 @@ type RobotCreate struct { ProjectID int64 `json:"pid"` Description string `json:"description"` Disabled bool `json:"disabled"` + ExpiresAt int64 `json:"expires_at"` Visible bool `json:"-"` Access []*rbac.Policy `json:"access"` } @@ -67,6 +68,9 @@ func (rq *RobotCreate) Valid(v *validation.Validation) { if utils.IsContainIllegalChar(rq.Name, []string{",", "~", "#", "$", "%"}) { v.SetError("name", "robot name contains illegal characters") } + if rq.ExpiresAt < 0 { + v.SetError("expires_at", "expiration time must be a positive integer if set") + } } // RobotRep ... diff --git a/tests/apitests/python/library/project.py b/tests/apitests/python/library/project.py index ee9969c28..96d282670 100644 --- a/tests/apitests/python/library/project.py +++ b/tests/apitests/python/library/project.py @@ -173,7 +173,7 @@ class Project(base.Base): base._assert_status_code(expect_status_code, status_code) return base._get_id_from_header(header) - def add_project_robot_account(self, project_id, project_name, robot_name = None, robot_desc = None, has_pull_right = True, has_push_right = True, expect_status_code = 201, **kwargs): + 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, expect_status_code = 201, **kwargs): if robot_name is None: robot_name = base._random_name("robot") if robot_desc is None: @@ -190,7 +190,7 @@ class Project(base.Base): if has_push_right is True: robotAccountAccess = swagger_client.RobotAccountAccess(resource = resource_by_project_id, action = action_push) access_list.append(robotAccountAccess) - robotAccountCreate = swagger_client.RobotAccountCreate(robot_name, robot_desc, access_list) + 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) diff --git a/tests/apitests/python/test_robot_account.py b/tests/apitests/python/test_robot_account.py index 7dc03f9f0..74fef524a 100644 --- a/tests/apitests/python/test_robot_account.py +++ b/tests/apitests/python/test_robot_account.py @@ -93,7 +93,8 @@ class TestProjects(unittest.TestCase): TestProjects.repo_name_in_project_c, tag_c = push_image_to_project(project_ra_name_c, harbor_server, user_ra_name, user_ra_password, image_project_c, tag) print "#4. Create a new robot account(RA) with pull and push privilige in project(PA) by user(UA);" - robot_id, robot_account = self.project.add_project_robot_account(TestProjects.project_ra_id_a, project_ra_name_a, **TestProjects.USER_RA_CLIENT) + robot_id, robot_account = self.project.add_project_robot_account(TestProjects.project_ra_id_a, project_ra_name_a, + 2441000531 ,**TestProjects.USER_RA_CLIENT) print robot_account.name print robot_account.token