add expiration of robot account

This commit is to make the expiration of robot account configurable

1, The expiration could be set by system admin in the configuation page or
by /api/config with robot_token_expiration=60, the default value is 30 days.
2, The expiration could be shown in the robot account infor both on UI and API.

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
wang yan 2019-02-20 16:51:36 +08:00
parent 4cb49e5388
commit 47a09b5891
13 changed files with 54 additions and 28 deletions

View File

@ -4734,6 +4734,9 @@ definitions:
description: description:
type: string type: string
description: The description of robot account description: The description of robot account
expiration:
type: integer
description: The expiration of robot account
project_id: project_id:
type: integer type: integer
description: The project id of robot account description: The project id of robot account

View File

@ -3,6 +3,7 @@ CREATE TABLE robot (
name varchar(255), name varchar(255),
description varchar(1024), description varchar(1024),
project_id int, project_id int,
expiration int,
disabled boolean DEFAULT false NOT NULL, disabled boolean DEFAULT false NOT NULL,
creation_time timestamp default CURRENT_TIMESTAMP, creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP, update_time timestamp default CURRENT_TIMESTAMP,

View File

@ -51,7 +51,6 @@ const (
) )
var ( var (
// ConfigList - All configure items used in harbor // ConfigList - All configure items used in harbor
// Steps to onboard a new setting // Steps to onboard a new setting
// 1. Add configure item in metadatalist.go // 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_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_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: "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},
} }
) )

View File

@ -119,4 +119,5 @@ const (
// Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user. // Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user.
RobotPrefix = "robot$" RobotPrefix = "robot$"
CoreConfigPath = "/api/internal/configurations" CoreConfigPath = "/api/internal/configurations"
RobotTokenExpiration = "robot_token_expiration"
) )

View File

@ -29,6 +29,7 @@ type Robot struct {
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name"`
Description string `orm:"column(description)" json:"description"` Description string `orm:"column(description)" json:"description"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"` ProjectID int64 `orm:"column(project_id)" json:"project_id"`
Expiration int64 `orm:"column(expiration)" json:"expiration"`
Disabled bool `orm:"column(disabled)" json:"disabled"` Disabled bool `orm:"column(disabled)" json:"disabled"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` 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"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`

View File

@ -25,5 +25,9 @@ func (rc RobotClaims) Valid() error {
if rc.Access == nil { if rc.Access == nil {
return errors.New("The access info cannot be nil") return errors.New("The access info cannot be nil")
} }
stdErr := rc.StandardClaims.Valid()
if stdErr != nil {
return stdErr
}
return nil return nil
} }

View File

@ -19,14 +19,15 @@ type HToken struct {
} }
// New ... // New ...
func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) { func New(tokenID, projectID, expiresAt int64, access []*rbac.Policy) (*HToken, error) {
rClaims := &RobotClaims{ rClaims := &RobotClaims{
TokenID: tokenID, TokenID: tokenID,
ProjectID: projectID, ProjectID: projectID,
Access: access, Access: access,
StandardClaims: jwt.StandardClaims{ StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(DefaultOptions.TTL).Unix(), IssuedAt: time.Now().UTC().Unix(),
Issuer: DefaultOptions.Issuer, ExpiresAt: expiresAt,
Issuer: DefaultOptions().Issuer,
}, },
} }
err := rClaims.Valid() err := rClaims.Valid()
@ -34,13 +35,13 @@ func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) {
return nil, err return nil, err
} }
return &HToken{ return &HToken{
Token: *jwt.NewWithClaims(DefaultOptions.SignMethod, rClaims), Token: *jwt.NewWithClaims(DefaultOptions().SignMethod, rClaims),
}, nil }, nil
} }
// Raw get the Raw string of token // Raw get the Raw string of token
func (htk *HToken) Raw() (string, error) { func (htk *HToken) Raw() (string, error) {
key, err := DefaultOptions.GetKey() key, err := DefaultOptions().GetKey()
if err != nil { if err != nil {
return "", nil return "", nil
} }
@ -54,12 +55,12 @@ func (htk *HToken) Raw() (string, error) {
// ParseWithClaims ... // ParseWithClaims ...
func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) { func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) {
key, err := DefaultOptions.GetKey() key, err := DefaultOptions().GetKey()
if err != nil { if err != nil {
return nil, err return nil, err
} }
token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) { 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") return nil, errors.New("invalid signing method")
} }
switch k := key.(type) { 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)) log.Errorf(fmt.Sprintf("parse token error, %v", err))
return nil, err return nil, err
} }
if !token.Valid { if !token.Valid {
log.Errorf(fmt.Sprintf("invalid jwt token, %v", token)) log.Errorf(fmt.Sprintf("invalid jwt token, %v", token))
return nil, err return nil, errors.New("invalid jwt token")
} }
return &HToken{ return &HToken{
Token: *token, Token: *token,

View File

@ -3,6 +3,7 @@ package token
import ( import (
"os" "os"
"testing" "testing"
"time"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
@ -30,7 +31,9 @@ func TestNew(t *testing.T) {
tokenID := int64(123) tokenID := int64(123)
projectID := int64(321) 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.Nil(t, err)
assert.Equal(t, token.Header["alg"], "RS256") assert.Equal(t, token.Header["alg"], "RS256")
@ -49,7 +52,9 @@ func TestRaw(t *testing.T) {
tokenID := int64(123) tokenID := int64(123)
projectID := int64(321) 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.Nil(t, err)
rawTk, err := token.Raw() rawTk, err := token.Raw()

View File

@ -16,12 +16,6 @@ const (
signedMethod = "RS256" signedMethod = "RS256"
) )
var (
privateKey = config.TokenPrivateKeyPath()
// DefaultOptions ...
DefaultOptions = NewOptions()
)
// Options ... // Options ...
type Options struct { type Options struct {
SignMethod jwt.SigningMethod SignMethod jwt.SigningMethod
@ -31,9 +25,10 @@ type Options struct {
Issuer string Issuer string
} }
// NewOptions ... // DefaultOptions ...
func NewOptions() *Options { func DefaultOptions() *Options {
privateKey, err := ioutil.ReadFile(privateKey) privateKeyFile := config.TokenPrivateKeyPath()
privateKey, err := ioutil.ReadFile(privateKeyFile)
if err != nil { if err != nil {
log.Errorf(fmt.Sprintf("failed to read private key %v", err)) log.Errorf(fmt.Sprintf("failed to read private key %v", err))
return nil return nil

View File

@ -8,7 +8,7 @@ import (
) )
func TestNewOptions(t *testing.T) { func TestNewOptions(t *testing.T) {
defaultOpt := DefaultOptions defaultOpt := DefaultOptions()
assert.NotNil(t, defaultOpt) assert.NotNil(t, defaultOpt)
assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256")) assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256"))
assert.Equal(t, defaultOpt.Issuer, "harbor-token-issuer") assert.Equal(t, defaultOpt.Issuer, "harbor-token-issuer")
@ -16,7 +16,7 @@ func TestNewOptions(t *testing.T) {
} }
func TestGetKey(t *testing.T) { func TestGetKey(t *testing.T) {
defaultOpt := DefaultOptions defaultOpt := DefaultOptions()
key, err := defaultOpt.GetKey() key, err := defaultOpt.GetKey()
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, key) assert.NotNil(t, key)

View File

@ -16,14 +16,16 @@ package api
import ( import (
"fmt" "fmt"
"net/http"
"strconv"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"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/common/token" "github.com/goharbor/harbor/src/common/token"
"net/http"
"strconv"
"github.com/goharbor/harbor/src/core/config"
"time"
) )
// RobotAPI ... // RobotAPI ...
@ -104,6 +106,9 @@ func (r *RobotAPI) Post() {
} }
var robotReq models.RobotReq 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) r.DecodeJSONReq(&robotReq)
createdName := common.RobotPrefix + robotReq.Name createdName := common.RobotPrefix + robotReq.Name
@ -112,6 +117,7 @@ func (r *RobotAPI) Post() {
Name: createdName, Name: createdName,
Description: robotReq.Description, Description: robotReq.Description,
ProjectID: r.project.ProjectID, ProjectID: r.project.ProjectID,
Expiration: expiresAt,
} }
id, err := dao.AddRobot(&robot) id, err := dao.AddRobot(&robot)
if err != nil { if err != nil {
@ -125,7 +131,7 @@ func (r *RobotAPI) Post() {
// generate the token, and return it with response data. // generate the token, and return it with response data.
// token is not stored in the database. // 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 { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err)) r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err))
err := dao.DeleteRobot(id) err := dao.DeleteRobot(id)

View File

@ -225,6 +225,11 @@ func TokenExpiration() (int, error) {
return cfgMgr.Get(common.TokenExpiration).GetInt(), nil 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 // ExtEndpoint returns the external URL of Harbor: protocol://host:port
func ExtEndpoint() (string, error) { func ExtEndpoint() (string, error) {
return cfgMgr.Get(common.ExtEndpoint).GetString(), nil return cfgMgr.Get(common.ExtEndpoint).GetString(), nil

View File

@ -97,6 +97,9 @@ func TestConfig(t *testing.T) {
t.Fatalf("failed to get token expiration: %v", err) t.Fatalf("failed to get token expiration: %v", err)
} }
tkExp := RobotTokenExpiration()
assert.Equal(tkExp, 30)
if _, err := ExtEndpoint(); err != nil { if _, err := ExtEndpoint(); err != nil {
t.Fatalf("failed to get domain name: %v", err) t.Fatalf("failed to get domain name: %v", err)
} }