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:
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

View File

@ -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,

View File

@ -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},
}
)

View File

@ -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"
)

View File

@ -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"`

View File

@ -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
}

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)
}