diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 061c98c87..7836425e2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -4734,6 +4734,9 @@ definitions: description: type: string description: The description of robot account + expiration: + type: integer + description: The expiration of robot account project_id: type: integer description: The project id of robot account diff --git a/make/migrations/postgresql/0004_add_robot_account.up.sql b/make/migrations/postgresql/0004_add_robot_account.up.sql index c2e3d273b..4c2579a3d 100644 --- a/make/migrations/postgresql/0004_add_robot_account.up.sql +++ b/make/migrations/postgresql/0004_add_robot_account.up.sql @@ -3,6 +3,7 @@ CREATE TABLE robot ( name varchar(255), description varchar(1024), project_id int, + expiration int, disabled boolean DEFAULT false NOT NULL, creation_time timestamp default CURRENT_TIMESTAMP, update_time timestamp default CURRENT_TIMESTAMP, diff --git a/src/common/config/metadata/metadatalist.go b/src/common/config/metadata/metadatalist.go index 1c54a015c..7e253f50d 100644 --- a/src/common/config/metadata/metadatalist.go +++ b/src/common/config/metadata/metadatalist.go @@ -51,7 +51,6 @@ const ( ) var ( - // ConfigList - All configure items used in harbor // Steps to onboard a new setting // 1. Add configure item in metadatalist.go @@ -131,5 +130,6 @@ var ( {Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, {Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, {Name: "with_notary", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_NOTARY", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, + {Name: "robot_token_expiration", Scope: UserScope, Group: BasicGroup, EnvKey: "ROBOT_TOKEN_EXPIRATION", DefaultValue: "30", ItemType: &IntType{}, Editable: true}, } ) diff --git a/src/common/const.go b/src/common/const.go index f6ae886ad..0875f90c3 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -117,6 +117,7 @@ const ( DefaultRegistryCtlURL = "http://registryctl:8080" DefaultClairHealthCheckServerURL = "http://clair:6061" // Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user. - RobotPrefix = "robot$" - CoreConfigPath = "/api/internal/configurations" + RobotPrefix = "robot$" + CoreConfigPath = "/api/internal/configurations" + RobotTokenExpiration = "robot_token_expiration" ) diff --git a/src/common/models/robot.go b/src/common/models/robot.go index 6998c4a3f..ec85c5dd2 100644 --- a/src/common/models/robot.go +++ b/src/common/models/robot.go @@ -29,6 +29,7 @@ type Robot struct { Name string `orm:"column(name)" json:"name"` Description string `orm:"column(description)" json:"description"` ProjectID int64 `orm:"column(project_id)" json:"project_id"` + Expiration int64 `orm:"column(expiration)" json:"expiration"` Disabled bool `orm:"column(disabled)" json:"disabled"` 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"` diff --git a/src/common/token/claims.go b/src/common/token/claims.go index 4739f9d21..f8cd2dc65 100644 --- a/src/common/token/claims.go +++ b/src/common/token/claims.go @@ -25,5 +25,9 @@ func (rc RobotClaims) Valid() error { if rc.Access == nil { return errors.New("The access info cannot be nil") } + stdErr := rc.StandardClaims.Valid() + if stdErr != nil { + return stdErr + } return nil } diff --git a/src/common/token/htoken.go b/src/common/token/htoken.go index ac9067820..897c50467 100644 --- a/src/common/token/htoken.go +++ b/src/common/token/htoken.go @@ -19,14 +19,15 @@ type HToken struct { } // New ... -func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) { +func New(tokenID, projectID, expiresAt int64, access []*rbac.Policy) (*HToken, error) { rClaims := &RobotClaims{ TokenID: tokenID, ProjectID: projectID, Access: access, StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(DefaultOptions.TTL).Unix(), - Issuer: DefaultOptions.Issuer, + IssuedAt: time.Now().UTC().Unix(), + ExpiresAt: expiresAt, + Issuer: DefaultOptions().Issuer, }, } err := rClaims.Valid() @@ -34,13 +35,13 @@ func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) { return nil, err } return &HToken{ - Token: *jwt.NewWithClaims(DefaultOptions.SignMethod, rClaims), + Token: *jwt.NewWithClaims(DefaultOptions().SignMethod, rClaims), }, nil } // Raw get the Raw string of token func (htk *HToken) Raw() (string, error) { - key, err := DefaultOptions.GetKey() + key, err := DefaultOptions().GetKey() if err != nil { return "", nil } @@ -54,12 +55,12 @@ func (htk *HToken) Raw() (string, error) { // ParseWithClaims ... func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) { - key, err := DefaultOptions.GetKey() + key, err := DefaultOptions().GetKey() if err != nil { return nil, err } token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) { - if token.Method.Alg() != DefaultOptions.SignMethod.Alg() { + if token.Method.Alg() != DefaultOptions().SignMethod.Alg() { return nil, errors.New("invalid signing method") } switch k := key.(type) { @@ -75,9 +76,10 @@ func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) { log.Errorf(fmt.Sprintf("parse token error, %v", err)) return nil, err } + if !token.Valid { log.Errorf(fmt.Sprintf("invalid jwt token, %v", token)) - return nil, err + return nil, errors.New("invalid jwt token") } return &HToken{ Token: *token, diff --git a/src/common/token/htoken_test.go b/src/common/token/htoken_test.go index e2d81b10c..38e187ef7 100644 --- a/src/common/token/htoken_test.go +++ b/src/common/token/htoken_test.go @@ -3,6 +3,7 @@ package token import ( "os" "testing" + "time" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/core/config" @@ -30,7 +31,9 @@ func TestNew(t *testing.T) { tokenID := int64(123) projectID := int64(321) - token, err := New(tokenID, projectID, policies) + tokenExpiration := time.Duration(10) * 24 * time.Hour + expiresAt := time.Now().UTC().Add(tokenExpiration).Unix() + token, err := New(tokenID, projectID, expiresAt, policies) assert.Nil(t, err) assert.Equal(t, token.Header["alg"], "RS256") @@ -49,7 +52,9 @@ func TestRaw(t *testing.T) { tokenID := int64(123) projectID := int64(321) - token, err := New(tokenID, projectID, policies) + tokenExpiration := time.Duration(10) * 24 * time.Hour + expiresAt := time.Now().UTC().Add(tokenExpiration).Unix() + token, err := New(tokenID, projectID, expiresAt, policies) assert.Nil(t, err) rawTk, err := token.Raw() diff --git a/src/common/token/options.go b/src/common/token/options.go index a3328d82e..747d7435d 100644 --- a/src/common/token/options.go +++ b/src/common/token/options.go @@ -16,12 +16,6 @@ const ( signedMethod = "RS256" ) -var ( - privateKey = config.TokenPrivateKeyPath() - // DefaultOptions ... - DefaultOptions = NewOptions() -) - // Options ... type Options struct { SignMethod jwt.SigningMethod @@ -31,9 +25,10 @@ type Options struct { Issuer string } -// NewOptions ... -func NewOptions() *Options { - privateKey, err := ioutil.ReadFile(privateKey) +// DefaultOptions ... +func DefaultOptions() *Options { + privateKeyFile := config.TokenPrivateKeyPath() + privateKey, err := ioutil.ReadFile(privateKeyFile) if err != nil { log.Errorf(fmt.Sprintf("failed to read private key %v", err)) return nil diff --git a/src/common/token/options_test.go b/src/common/token/options_test.go index 660975fff..5f64fb380 100644 --- a/src/common/token/options_test.go +++ b/src/common/token/options_test.go @@ -8,7 +8,7 @@ import ( ) func TestNewOptions(t *testing.T) { - defaultOpt := DefaultOptions + defaultOpt := DefaultOptions() assert.NotNil(t, defaultOpt) assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256")) assert.Equal(t, defaultOpt.Issuer, "harbor-token-issuer") @@ -16,7 +16,7 @@ func TestNewOptions(t *testing.T) { } func TestGetKey(t *testing.T) { - defaultOpt := DefaultOptions + defaultOpt := DefaultOptions() key, err := defaultOpt.GetKey() assert.Nil(t, err) assert.NotNil(t, key) diff --git a/src/core/api/robot.go b/src/core/api/robot.go index 03850f90f..2cf5f28e0 100644 --- a/src/core/api/robot.go +++ b/src/core/api/robot.go @@ -16,14 +16,16 @@ package api import ( "fmt" - "net/http" - "strconv" - "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/token" + "net/http" + "strconv" + + "github.com/goharbor/harbor/src/core/config" + "time" ) // RobotAPI ... @@ -104,6 +106,9 @@ func (r *RobotAPI) Post() { } var robotReq models.RobotReq + // Expiration in days + tokenExpiration := time.Duration(config.RobotTokenExpiration()) * 24 * time.Hour + expiresAt := time.Now().UTC().Add(tokenExpiration).Unix() r.DecodeJSONReq(&robotReq) createdName := common.RobotPrefix + robotReq.Name @@ -112,6 +117,7 @@ func (r *RobotAPI) Post() { Name: createdName, Description: robotReq.Description, ProjectID: r.project.ProjectID, + Expiration: expiresAt, } id, err := dao.AddRobot(&robot) if err != nil { @@ -125,7 +131,7 @@ func (r *RobotAPI) Post() { // generate the token, and return it with response data. // token is not stored in the database. - jwtToken, err := token.New(id, r.project.ProjectID, robotReq.Access) + jwtToken, err := token.New(id, r.project.ProjectID, expiresAt, robotReq.Access) if err != nil { r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err)) err := dao.DeleteRobot(id) diff --git a/src/core/config/config.go b/src/core/config/config.go index d90b61751..e693d06c1 100644 --- a/src/core/config/config.go +++ b/src/core/config/config.go @@ -225,6 +225,11 @@ func TokenExpiration() (int, error) { return cfgMgr.Get(common.TokenExpiration).GetInt(), nil } +// RobotTokenExpiration returns the token expiration time of robot account (in day) +func RobotTokenExpiration() int { + return cfgMgr.Get(common.RobotTokenExpiration).GetInt() +} + // ExtEndpoint returns the external URL of Harbor: protocol://host:port func ExtEndpoint() (string, error) { return cfgMgr.Get(common.ExtEndpoint).GetString(), nil diff --git a/src/core/config/config_test.go b/src/core/config/config_test.go index 549dc37d5..41b3b5064 100644 --- a/src/core/config/config_test.go +++ b/src/core/config/config_test.go @@ -97,6 +97,9 @@ func TestConfig(t *testing.T) { t.Fatalf("failed to get token expiration: %v", err) } + tkExp := RobotTokenExpiration() + assert.Equal(tkExp, 30) + if _, err := ExtEndpoint(); err != nil { t.Fatalf("failed to get domain name: %v", err) }