Merge pull request #7004 from wy65701436/robot-expiration-time

add expiration of robot account
This commit is contained in:
Daniel Jiang 2019-02-25 13:48:11 +08:00 committed by GitHub
commit f69e4f9a4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 55 additions and 28 deletions

View File

@ -4734,6 +4734,9 @@ definitions:
description:
type: string
description: The description of robot account
expiresat:
type: integer
description: The expiration of robot account (in seconds)
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,
expiresat bigint,
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,7 @@ 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},
// the unit of expiration is minute, 43200 minutes = 30 days
{Name: "robot_token_duration", Scope: UserScope, Group: BasicGroup, EnvKey: "ROBOT_TOKEN_DURATION", DefaultValue: "43200", 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"
RobotTokenDuration = "robot_token_duration"
)

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"`
ExpiresAt int64 `orm:"column(expiresat)" json:"expiresat"`
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
// Token duration in minutes
tokenDuration := time.Duration(config.RobotTokenDuration()) * time.Minute
expiresAt := time.Now().UTC().Add(tokenDuration).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,
ExpiresAt: 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
}
// RobotTokenDuration returns the token expiration time of robot account (in minute)
func RobotTokenDuration() int {
return cfgMgr.Get(common.RobotTokenDuration).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 := RobotTokenDuration()
assert.Equal(tkExp, 43200)
if _, err := ExtEndpoint(); err != nil {
t.Fatalf("failed to get domain name: %v", err)
}