mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-18 22:57:38 +01:00
Enable robot account bypass policy check
1, the commit is for internal robot to bypass policy check, like vul and signature checking. 2, add a bool attribute into registry token, decode it in the harbor core and add the status into request context. 3, add a bool attribut for robot API controller, but API will not use it.y Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
cf87408e90
commit
22b4ea0f89
@ -64,6 +64,11 @@ func (s *SecurityContext) IsSysAdmin() bool {
|
||||
return s.ctx.IsSysAdmin()
|
||||
}
|
||||
|
||||
// PolicyCheck ...
|
||||
func (s *SecurityContext) PolicyCheck() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSolutionUser ...
|
||||
func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
|
@ -27,6 +27,8 @@ type Context interface {
|
||||
GetUsername() string
|
||||
// IsSysAdmin returns whether the user is system admin
|
||||
IsSysAdmin() bool
|
||||
// PolicyCheck returns whether the middlerwares apply to the request
|
||||
PolicyCheck() bool
|
||||
// IsSolutionUser returns whether the user is solution user
|
||||
IsSolutionUser() bool
|
||||
// Get current user's all project
|
||||
|
@ -61,6 +61,11 @@ func (s *SecurityContext) IsSysAdmin() bool {
|
||||
return s.user.HasAdminRole
|
||||
}
|
||||
|
||||
// PolicyCheck ...
|
||||
func (s *SecurityContext) PolicyCheck() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSolutionUser ...
|
||||
func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
|
@ -23,17 +23,19 @@ import (
|
||||
|
||||
// SecurityContext implements security.Context interface based on database
|
||||
type SecurityContext struct {
|
||||
robot *model.Robot
|
||||
pm promgr.ProjectManager
|
||||
policy []*rbac.Policy
|
||||
robot *model.Robot
|
||||
pm promgr.ProjectManager
|
||||
policy []*rbac.Policy
|
||||
polichCheck bool
|
||||
}
|
||||
|
||||
// NewSecurityContext ...
|
||||
func NewSecurityContext(robot *model.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext {
|
||||
func NewSecurityContext(robot *model.Robot, pm promgr.ProjectManager, policy []*rbac.Policy, polichCheck bool) *SecurityContext {
|
||||
return &SecurityContext{
|
||||
robot: robot,
|
||||
pm: pm,
|
||||
policy: policy,
|
||||
robot: robot,
|
||||
pm: pm,
|
||||
policy: policy,
|
||||
polichCheck: polichCheck,
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +58,11 @@ func (s *SecurityContext) IsSysAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PolicyCheck ...
|
||||
func (s *SecurityContext) PolicyCheck() bool {
|
||||
return s.polichCheck
|
||||
}
|
||||
|
||||
// IsSolutionUser robot cannot be a system admin
|
||||
func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
|
@ -93,45 +93,45 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestIsAuthenticated(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
ctx := NewSecurityContext(nil, nil, nil, true)
|
||||
assert.False(t, ctx.IsAuthenticated())
|
||||
|
||||
// authenticated
|
||||
ctx = NewSecurityContext(&model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, nil, nil)
|
||||
}, nil, nil, true)
|
||||
assert.True(t, ctx.IsAuthenticated())
|
||||
}
|
||||
|
||||
func TestGetUsername(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
ctx := NewSecurityContext(nil, nil, nil, true)
|
||||
assert.Equal(t, "", ctx.GetUsername())
|
||||
|
||||
// authenticated
|
||||
ctx = NewSecurityContext(&model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, nil, nil)
|
||||
}, nil, nil, true)
|
||||
assert.Equal(t, "test", ctx.GetUsername())
|
||||
}
|
||||
|
||||
func TestIsSysAdmin(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
ctx := NewSecurityContext(nil, nil, nil, true)
|
||||
assert.False(t, ctx.IsSysAdmin())
|
||||
|
||||
// authenticated, non admin
|
||||
ctx = NewSecurityContext(&model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, nil, nil)
|
||||
}, nil, nil, true)
|
||||
assert.False(t, ctx.IsSysAdmin())
|
||||
}
|
||||
|
||||
func TestIsSolutionUser(t *testing.T) {
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
ctx := NewSecurityContext(nil, nil, nil, true)
|
||||
assert.False(t, ctx.IsSolutionUser())
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ func TestHasPullPerm(t *testing.T) {
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctx := NewSecurityContext(robot, pm, policies)
|
||||
ctx := NewSecurityContext(robot, pm, policies, true)
|
||||
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(rbac.ActionPull, resource))
|
||||
}
|
||||
@ -164,7 +164,7 @@ func TestHasPushPerm(t *testing.T) {
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctx := NewSecurityContext(robot, pm, policies)
|
||||
ctx := NewSecurityContext(robot, pm, policies, true)
|
||||
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(rbac.ActionPush, resource))
|
||||
}
|
||||
@ -185,20 +185,20 @@ func TestHasPushPullPerm(t *testing.T) {
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctx := NewSecurityContext(robot, pm, policies)
|
||||
ctx := NewSecurityContext(robot, pm, policies, true)
|
||||
resource := rbac.NewProjectNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(rbac.ActionPush, resource) && ctx.Can(rbac.ActionPull, resource))
|
||||
}
|
||||
|
||||
func TestGetMyProjects(t *testing.T) {
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
ctx := NewSecurityContext(nil, nil, nil, true)
|
||||
projects, err := ctx.GetMyProjects()
|
||||
require.Nil(t, err)
|
||||
assert.Nil(t, projects)
|
||||
}
|
||||
|
||||
func TestGetProjectRoles(t *testing.T) {
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
ctx := NewSecurityContext(nil, nil, nil, true)
|
||||
roles := ctx.GetProjectRoles("test")
|
||||
assert.Nil(t, roles)
|
||||
}
|
||||
|
@ -66,6 +66,11 @@ func (s *SecurityContext) IsSysAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PolicyCheck ...
|
||||
func (s *SecurityContext) PolicyCheck() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSolutionUser ...
|
||||
func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return s.IsAuthenticated()
|
||||
|
@ -1,87 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HToken htoken is a jwt token for harbor robot account,
|
||||
// which contains the robot ID, project ID and the access permission for the project.
|
||||
// It used for authn/authz for robot account in Harbor.
|
||||
type HToken struct {
|
||||
jwt.Token
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(tokenID, projectID, expiresAt int64, access []*rbac.Policy) (*HToken, error) {
|
||||
rClaims := &RobotClaims{
|
||||
TokenID: tokenID,
|
||||
ProjectID: projectID,
|
||||
Access: access,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: time.Now().UTC().Unix(),
|
||||
ExpiresAt: expiresAt,
|
||||
Issuer: DefaultOptions().Issuer,
|
||||
},
|
||||
}
|
||||
err := rClaims.Valid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HToken{
|
||||
Token: *jwt.NewWithClaims(DefaultOptions().SignMethod, rClaims),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Raw get the Raw string of token
|
||||
func (htk *HToken) Raw() (string, error) {
|
||||
key, err := DefaultOptions().GetKey()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
raw, err := htk.Token.SignedString(key)
|
||||
if err != nil {
|
||||
log.Debugf(fmt.Sprintf("failed to issue token %v", err))
|
||||
return "", err
|
||||
}
|
||||
return raw, err
|
||||
}
|
||||
|
||||
// ParseWithClaims ...
|
||||
func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) {
|
||||
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() {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
switch k := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
default:
|
||||
return key, nil
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
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, errors.New("invalid jwt token")
|
||||
}
|
||||
return &HToken{
|
||||
Token: *token,
|
||||
}, nil
|
||||
}
|
@ -76,7 +76,7 @@ func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model.
|
||||
Type: "repository",
|
||||
Name: fqRepo,
|
||||
Actions: []string{"pull"},
|
||||
}})
|
||||
}}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -346,7 +346,7 @@ type rawTokenGenerator struct {
|
||||
|
||||
// generate token directly
|
||||
func (r *rawTokenGenerator) generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) {
|
||||
return token_util.MakeToken(r.username, r.service, scopes)
|
||||
return token_util.MakeToken(r.username, r.service, scopes, true)
|
||||
}
|
||||
|
||||
func buildPingURL(endpoint string) string {
|
||||
|
@ -108,6 +108,7 @@ func (r *RobotAPI) Post() {
|
||||
return
|
||||
}
|
||||
robotReq.Visible = true
|
||||
robotReq.PolicyCheck = true
|
||||
robotReq.ProjectID = r.project.ProjectID
|
||||
|
||||
if err := validateRobotReq(r.project, &robotReq); err != nil {
|
||||
|
@ -34,7 +34,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
|
||||
"github.com/goharbor/harbor/src/common/security/secret"
|
||||
"github.com/goharbor/harbor/src/common/token"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
@ -44,6 +43,8 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/authproxy"
|
||||
"github.com/goharbor/harbor/src/pkg/robot"
|
||||
pkg_token "github.com/goharbor/harbor/src/pkg/token"
|
||||
"github.com/goharbor/harbor/src/pkg/token/claim"
|
||||
)
|
||||
|
||||
// ContextValueKey for content value
|
||||
@ -188,15 +189,17 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
if !strings.HasPrefix(robotName, common.RobotPrefix) {
|
||||
return false
|
||||
}
|
||||
rClaims := &token.RobotClaims{}
|
||||
htk, err := token.ParseWithClaims(robotTk, rClaims)
|
||||
rClaims := &claim.Robot{}
|
||||
opt := pkg_token.DefaultTokenOptions()
|
||||
rtk, err := pkg_token.Parse(opt, robotTk, rClaims)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt robot token, %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
|
||||
ctr := robot.RobotCtr
|
||||
robot, err := ctr.GetRobotAccount(htk.Claims.(*token.RobotClaims).TokenID)
|
||||
robot, err := ctr.GetRobotAccount(rtk.Claims.(*claim.Robot).TokenID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get robot %s: %v", robotName, err)
|
||||
return false
|
||||
@ -215,7 +218,7 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
}
|
||||
log.Debug("creating robot account security context...")
|
||||
pm := config.GlobalProjectMgr
|
||||
securCtx := robotCtx.NewSecurityContext(robot, pm, htk.Claims.(*token.RobotClaims).Access)
|
||||
securCtx := robotCtx.NewSecurityContext(robot, pm, rtk.Claims.(*claim.Robot).Access, rClaims.PolicyCheck)
|
||||
setSecurCtxAndPM(ctx.Request, securCtx, pm)
|
||||
return true
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/readonly"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/regtoken"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/sizequota"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/url"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/vulnerable"
|
||||
@ -72,6 +73,7 @@ func (b *DefaultCreator) geMiddleware(mName string) alice.Constructor {
|
||||
SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) },
|
||||
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
|
||||
IMMUTABLE: func(next http.Handler) http.Handler { return immutable.New(next) },
|
||||
REGTOKEN: func(next http.Handler) http.Handler { return regtoken.New(next) },
|
||||
}
|
||||
return middlewares[mName]
|
||||
}
|
||||
|
@ -26,13 +26,14 @@ const (
|
||||
SIZEQUOTA = "sizequota"
|
||||
COUNTQUOTA = "countquota"
|
||||
IMMUTABLE = "immutable"
|
||||
REGTOKEN = "REGTOKEN"
|
||||
)
|
||||
|
||||
// ChartMiddlewares middlewares for chart server
|
||||
var ChartMiddlewares = []string{CHART}
|
||||
|
||||
// Middlewares with sequential organization
|
||||
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||
var Middlewares = []string{READONLY, URL, REGTOKEN, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||
|
||||
// MiddlewaresLocal ...
|
||||
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||
|
@ -49,6 +49,12 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
// Token bypass policy check
|
||||
policyCheck := req.Context().Value(util.PolicyCheckCtxKey)
|
||||
if policyCheck != nil && !policyCheck.(bool) {
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
if !util.GetPolicyChecker().ContentTrustEnabled(img.ProjectName) {
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
return
|
||||
|
55
src/core/middlewares/regtoken/handler.go
Normal file
55
src/core/middlewares/regtoken/handler.go
Normal file
@ -0,0 +1,55 @@
|
||||
package regtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||
pkg_token "github.com/goharbor/harbor/src/pkg/token"
|
||||
"github.com/goharbor/harbor/src/pkg/token/claim"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type regTokenHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(next http.Handler) http.Handler {
|
||||
return ®TokenHandler{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP ...
|
||||
func (r *regTokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
imgRaw := req.Context().Value(util.ImageInfoCtxKey)
|
||||
if imgRaw == nil || !config.WithClair() {
|
||||
r.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
img, _ := req.Context().Value(util.ImageInfoCtxKey).(util.ImageInfo)
|
||||
if img.Digest == "" {
|
||||
r.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(req.Header.Get("Authorization"), " ")
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
r.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
rawToken := parts[1]
|
||||
opt := pkg_token.DefaultTokenOptions()
|
||||
rClaims := &claim.Registry{}
|
||||
rtk, err := pkg_token.Parse(opt, rawToken, rClaims)
|
||||
if err != nil {
|
||||
log.Debug("failed to decode reg token: %v, the error is skipped and round the request to native registry.", err)
|
||||
r.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), util.PolicyCheckCtxKey, rtk.Claims.(*claim.Registry).PolicyCheck)
|
||||
req = req.WithContext(ctx)
|
||||
r.next.ServeHTTP(rw, req)
|
||||
}
|
@ -49,6 +49,8 @@ type contextKey string
|
||||
const (
|
||||
// ImageInfoCtxKey the context key for image information
|
||||
ImageInfoCtxKey = contextKey("ImageInfo")
|
||||
// PolicyCheckCtxKey the context key for image information
|
||||
PolicyCheckCtxKey = contextKey("PolicyCheck")
|
||||
// TokenUsername ...
|
||||
// TODO: temp solution, remove after vmware/harbor#2242 is resolved.
|
||||
TokenUsername = "harbor-core"
|
||||
|
@ -52,6 +52,13 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Token bypass policy check
|
||||
policyCheck := req.Context().Value(util.PolicyCheckCtxKey)
|
||||
if policyCheck != nil && !policyCheck.(bool) {
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Is vulnerable policy set?
|
||||
projectVulnerableEnabled, projectVulnerableSeverity, wl := util.GetPolicyChecker().VulnerablePolicy(img.ProjectName)
|
||||
if !projectVulnerableEnabled {
|
||||
|
@ -42,6 +42,12 @@ func init() {
|
||||
privateKey = config.TokenPrivateKeyPath()
|
||||
}
|
||||
|
||||
// ClaimSet ...
|
||||
type ClaimSet struct {
|
||||
token.ClaimSet
|
||||
PolicyCheck bool `json:"policy_check"`
|
||||
}
|
||||
|
||||
// GetResourceActions ...
|
||||
func GetResourceActions(scopes []string) []*token.ResourceActions {
|
||||
log.Debugf("scopes: %+v", scopes)
|
||||
@ -100,7 +106,7 @@ func filterAccess(access []*token.ResourceActions, ctx security.Context,
|
||||
}
|
||||
|
||||
// MakeToken makes a valid jwt token based on parms.
|
||||
func MakeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) {
|
||||
func MakeToken(username, service string, access []*token.ResourceActions, policyCheck bool) (*models.Token, error) {
|
||||
pk, err := libtrust.LoadKeyFile(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -110,7 +116,7 @@ func MakeToken(username, service string, access []*token.ResourceActions) (*mode
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk)
|
||||
tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk, policyCheck)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -138,7 +144,7 @@ func permToActions(p string) []string {
|
||||
|
||||
// make token core
|
||||
func makeTokenCore(issuer, subject, audience string, expiration int,
|
||||
access []*token.ResourceActions, signingKey libtrust.PrivateKey) (t *token.Token, expiresIn int, issuedAt *time.Time, err error) {
|
||||
access []*token.ResourceActions, signingKey libtrust.PrivateKey, policyCheck bool) (t *token.Token, expiresIn int, issuedAt *time.Time, err error) {
|
||||
|
||||
joseHeader := &token.Header{
|
||||
Type: "JWT",
|
||||
@ -155,15 +161,18 @@ func makeTokenCore(issuer, subject, audience string, expiration int,
|
||||
issuedAt = &now
|
||||
expiresIn = expiration * 60
|
||||
|
||||
claimSet := &token.ClaimSet{
|
||||
Issuer: issuer,
|
||||
Subject: subject,
|
||||
Audience: audience,
|
||||
Expiration: now.Add(time.Duration(expiration) * time.Minute).Unix(),
|
||||
NotBefore: now.Unix(),
|
||||
IssuedAt: now.Unix(),
|
||||
JWTID: jwtID,
|
||||
Access: access,
|
||||
claimSet := &ClaimSet{
|
||||
ClaimSet: token.ClaimSet{
|
||||
Issuer: issuer,
|
||||
Subject: subject,
|
||||
Audience: audience,
|
||||
Expiration: now.Add(time.Duration(expiration) * time.Minute).Unix(),
|
||||
NotBefore: now.Unix(),
|
||||
IssuedAt: now.Unix(),
|
||||
JWTID: jwtID,
|
||||
Access: access,
|
||||
},
|
||||
PolicyCheck: policyCheck,
|
||||
}
|
||||
|
||||
var joseHeaderBytes, claimSetBytes []byte
|
||||
|
@ -222,7 +222,7 @@ func (g generalCreator) Create(r *http.Request) (*models.Token, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MakeToken(ctx.GetUsername(), g.service, access)
|
||||
return MakeToken(ctx.GetUsername(), g.service, access, ctx.PolicyCheck())
|
||||
}
|
||||
|
||||
func parseScopes(u *url.URL) []string {
|
||||
|
@ -119,7 +119,8 @@ func getPublicKey(crtPath string) (*rsa.PublicKey, error) {
|
||||
type harborClaims struct {
|
||||
jwt.StandardClaims
|
||||
// Private claims
|
||||
Access []*token.ResourceActions `json:"access"`
|
||||
Access []*token.ResourceActions `json:"access"`
|
||||
PolicyCheck bool `json:"policy_check"`
|
||||
}
|
||||
|
||||
func TestMakeToken(t *testing.T) {
|
||||
@ -133,7 +134,7 @@ func TestMakeToken(t *testing.T) {
|
||||
}}
|
||||
svc := "harbor-registry"
|
||||
u := "tester"
|
||||
tokenJSON, err := MakeToken(u, svc, ra)
|
||||
tokenJSON, err := MakeToken(u, svc, ra, true)
|
||||
if err != nil {
|
||||
t.Errorf("Error while making token: %v", err)
|
||||
}
|
||||
@ -154,6 +155,7 @@ func TestMakeToken(t *testing.T) {
|
||||
t.Errorf("Error while parsing the token: %v", err)
|
||||
}
|
||||
claims := tok.Claims.(*harborClaims)
|
||||
assert.True(t, claims.PolicyCheck)
|
||||
assert.Equal(t, *(claims.Access[0]), *(ra[0]), "Access mismatch")
|
||||
assert.Equal(t, claims.Audience, svc, "Audience mismatch")
|
||||
}
|
||||
@ -242,6 +244,9 @@ func (f *fakeSecurityContext) IsSysAdmin() bool {
|
||||
func (f *fakeSecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
}
|
||||
func (f *fakeSecurityContext) PolicyCheck() bool {
|
||||
return false
|
||||
}
|
||||
func (f *fakeSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
return false
|
||||
}
|
||||
|
@ -2,12 +2,14 @@ package robot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/token"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/token"
|
||||
"github.com/goharbor/harbor/src/pkg/token/claim"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
@ -76,13 +78,24 @@ func (d *DefaultAPIController) CreateRobotAccount(robotReq *model.RobotCreate) (
|
||||
|
||||
// generate the token, and return it with response data.
|
||||
// token is not stored in the database.
|
||||
jwtToken, err := token.New(id, robotReq.ProjectID, expiresAt, robotReq.Access)
|
||||
opt := token.DefaultTokenOptions()
|
||||
rClaims := &claim.Robot{
|
||||
TokenID: id,
|
||||
ProjectID: robotReq.ProjectID,
|
||||
Access: robotReq.Access,
|
||||
PolicyCheck: robotReq.PolicyCheck,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: time.Now().UTC().Unix(),
|
||||
ExpiresAt: expiresAt,
|
||||
Issuer: opt.Issuer,
|
||||
},
|
||||
}
|
||||
tk, err := token.New(opt, rClaims)
|
||||
if err != nil {
|
||||
deferDel = err
|
||||
return nil, fmt.Errorf("failed to valid parameters to generate token for robot account, %v", err)
|
||||
}
|
||||
|
||||
rawTk, err := jwtToken.Raw()
|
||||
rawTk, err := tk.Raw()
|
||||
if err != nil {
|
||||
deferDel = err
|
||||
return nil, fmt.Errorf("failed to sign token for robot account, %v", err)
|
||||
|
@ -50,6 +50,7 @@ type RobotCreate struct {
|
||||
Description string `json:"description"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Visible bool `json:"-"`
|
||||
PolicyCheck bool `json:"-"`
|
||||
Access []*rbac.Policy `json:"access"`
|
||||
}
|
||||
|
||||
|
22
src/pkg/token/claim/registry.go
Normal file
22
src/pkg/token/claim/registry.go
Normal file
@ -0,0 +1,22 @@
|
||||
package claim
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/docker/distribution/registry/auth/token"
|
||||
)
|
||||
|
||||
// Registry implements the interface of jwt.Claims
|
||||
type Registry struct {
|
||||
jwt.StandardClaims
|
||||
PolicyCheck bool `json:"policy_check"`
|
||||
Access []*token.ResourceActions `json:"access"`
|
||||
}
|
||||
|
||||
// Valid valid the standard claims
|
||||
func (rc *Registry) Valid() error {
|
||||
stdErr := rc.StandardClaims.Valid()
|
||||
if stdErr != nil {
|
||||
return stdErr
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package token
|
||||
package claim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -6,16 +6,17 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
)
|
||||
|
||||
// RobotClaims implements the interface of jwt.Claims
|
||||
type RobotClaims struct {
|
||||
// Robot implements the interface of jwt.Claims
|
||||
type Robot struct {
|
||||
jwt.StandardClaims
|
||||
TokenID int64 `json:"id"`
|
||||
ProjectID int64 `json:"pid"`
|
||||
Access []*rbac.Policy `json:"access"`
|
||||
TokenID int64 `json:"id"`
|
||||
ProjectID int64 `json:"pid"`
|
||||
PolicyCheck bool `json:"policy_check"`
|
||||
Access []*rbac.Policy `json:"access"`
|
||||
}
|
||||
|
||||
// Valid valid the claims "tokenID, projectID and access".
|
||||
func (rc RobotClaims) Valid() error {
|
||||
func (rc Robot) Valid() error {
|
||||
if rc.TokenID < 0 {
|
||||
return errors.New("Token id must an valid INT")
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package token
|
||||
package claim
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
@ -15,7 +15,7 @@ func TestValid(t *testing.T) {
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
rClaims := &Robot{
|
||||
TokenID: 1,
|
||||
ProjectID: 2,
|
||||
Access: policies,
|
||||
@ -32,7 +32,7 @@ func TestUnValidTokenID(t *testing.T) {
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
rClaims := &Robot{
|
||||
TokenID: -1,
|
||||
ProjectID: 2,
|
||||
Access: policies,
|
||||
@ -49,7 +49,7 @@ func TestUnValidProjectID(t *testing.T) {
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
rClaims := &Robot{
|
||||
TokenID: 1,
|
||||
ProjectID: -2,
|
||||
Access: policies,
|
||||
@ -59,7 +59,7 @@ func TestUnValidProjectID(t *testing.T) {
|
||||
|
||||
func TestUnValidPolicy(t *testing.T) {
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
rClaims := &Robot{
|
||||
TokenID: 1,
|
||||
ProjectID: 2,
|
||||
Access: nil,
|
@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestNewOptions(t *testing.T) {
|
||||
defaultOpt := DefaultOptions()
|
||||
defaultOpt := DefaultTokenOptions()
|
||||
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 := DefaultTokenOptions()
|
||||
key, err := defaultOpt.GetKey()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, key)
|
@ -11,9 +11,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ttl = 60 * time.Minute
|
||||
issuer = "harbor-token-issuer"
|
||||
signedMethod = "RS256"
|
||||
defaultTTL = 60 * time.Minute
|
||||
defaultIssuer = "harbor-token-defaultIssuer"
|
||||
defaultSignedMethod = "RS256"
|
||||
)
|
||||
|
||||
// Options ...
|
||||
@ -25,23 +25,6 @@ type Options struct {
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
opt := &Options{
|
||||
SignMethod: jwt.GetSigningMethod(signedMethod),
|
||||
PrivateKey: privateKey,
|
||||
Issuer: issuer,
|
||||
TTL: ttl,
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
// GetKey ...
|
||||
func (o *Options) GetKey() (interface{}, error) {
|
||||
var err error
|
||||
@ -76,3 +59,20 @@ func (o *Options) GetKey() (interface{}, error) {
|
||||
return nil, fmt.Errorf(fmt.Sprintf("unsupported sign method, %s", o.SignMethod))
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultTokenOptions ...
|
||||
func DefaultTokenOptions() *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
|
||||
}
|
||||
opt := &Options{
|
||||
SignMethod: jwt.GetSigningMethod(defaultSignedMethod),
|
||||
PrivateKey: privateKey,
|
||||
Issuer: defaultIssuer,
|
||||
TTL: defaultTTL,
|
||||
}
|
||||
return opt
|
||||
}
|
77
src/pkg/token/token.go
Normal file
77
src/pkg/token/token.go
Normal file
@ -0,0 +1,77 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// Token is a jwt token for harbor robot account,
|
||||
type Token struct {
|
||||
jwt.Token
|
||||
Opt *Options
|
||||
Claim jwt.Claims
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(opt *Options, claims jwt.Claims) (*Token, error) {
|
||||
err := claims.Valid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Token{
|
||||
Token: *jwt.NewWithClaims(opt.SignMethod, claims),
|
||||
Opt: opt,
|
||||
Claim: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Raw get the Raw string of token
|
||||
func (tk *Token) Raw() (string, error) {
|
||||
key, err := tk.Opt.GetKey()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
raw, err := tk.Token.SignedString(key)
|
||||
if err != nil {
|
||||
log.Debugf(fmt.Sprintf("failed to issue token %v", err))
|
||||
return "", err
|
||||
}
|
||||
return raw, err
|
||||
}
|
||||
|
||||
// Parse ...
|
||||
func Parse(opt *Options, rawToken string, claims jwt.Claims) (*Token, error) {
|
||||
key, err := opt.GetKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if token.Method.Alg() != opt.SignMethod.Alg() {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
switch k := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
default:
|
||||
return key, nil
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
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, errors.New("invalid jwt token")
|
||||
}
|
||||
return &Token{
|
||||
Token: *token,
|
||||
}, nil
|
||||
}
|
@ -5,8 +5,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/pkg/token/claim"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -33,7 +35,15 @@ func TestNew(t *testing.T) {
|
||||
projectID := int64(321)
|
||||
tokenExpiration := time.Duration(10) * 24 * time.Hour
|
||||
expiresAt := time.Now().UTC().Add(tokenExpiration).Unix()
|
||||
token, err := New(tokenID, projectID, expiresAt, policies)
|
||||
robot := claim.Robot{
|
||||
TokenID: tokenID,
|
||||
ProjectID: projectID,
|
||||
Access: policies,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expiresAt,
|
||||
},
|
||||
}
|
||||
token, err := New(DefaultTokenOptions(), robot)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, token.Header["alg"], "RS256")
|
||||
@ -54,7 +64,15 @@ func TestRaw(t *testing.T) {
|
||||
|
||||
tokenExpiration := time.Duration(10) * 24 * time.Hour
|
||||
expiresAt := time.Now().UTC().Add(tokenExpiration).Unix()
|
||||
token, err := New(tokenID, projectID, expiresAt, policies)
|
||||
robot := claim.Robot{
|
||||
TokenID: tokenID,
|
||||
ProjectID: projectID,
|
||||
Access: policies,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expiresAt,
|
||||
},
|
||||
}
|
||||
token, err := New(DefaultTokenOptions(), robot)
|
||||
assert.Nil(t, err)
|
||||
|
||||
rawTk, err := token.Raw()
|
||||
@ -64,8 +82,8 @@ func TestRaw(t *testing.T) {
|
||||
|
||||
func TestParseWithClaims(t *testing.T) {
|
||||
rawTk := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTIzLCJQcm9qZWN0SUQiOjAsIkFjY2VzcyI6W3siUmVzb3VyY2UiOiIvcHJvamVjdC9saWJyYXkvcmVwb3NpdG9yeSIsIkFjdGlvbiI6InB1bGwiLCJFZmZlY3QiOiIifV0sIlN0YW5kYXJkQ2xhaW1zIjp7ImV4cCI6MTU0ODE0MDIyOSwiaXNzIjoiaGFyYm9yLXRva2VuLWlzc3VlciJ9fQ.Jc3qSKN4SJVUzAvBvemVpRcSOZaHlu0Avqms04qzPm4ru9-r9IRIl3mnSkI6m9XkzLUeJ7Kiwyw63ghngnVKw_PupeclOGC6s3TK5Cfmo4h-lflecXjZWwyy-dtH_e7Us_ItS-R3nXDJtzSLEpsGHCcAj-1X2s93RB2qD8LNSylvYeDezVkTzqRzzfawPJheKKh9JTrz-3eUxCwQard9-xjlwvfUYULoHTn9npNAUq4-jqhipW4uE8HL-ym33AGF57la8U0RO11hmDM5K8-PiYknbqJ_oONeS3HBNym2pEFeGjtTv2co213wl4T5lemlg4SGolMBuJ03L7_beVZ0o-MKTkKDqDwJalb6_PM-7u3RbxC9IzJMiwZKIPnD3FvV10iPxUUQHaH8Jz5UZ2pFIhi_8BNnlBfT0JOPFVYATtLjHMczZelj2YvAeR1UHBzq3E0jPpjjwlqIFgaHCaN_KMwEvadTo_Fi2sEH4pNGP7M3yehU_72oLJQgF4paJarsmEoij6ZtPs6xekBz1fccVitq_8WNIz9aeCUdkUBRwI5QKw1RdW4ua-w74ld5MZStWJA8veyoLkEb_Q9eq2oAj5KWFjJbW5-ltiIfM8gxKflsrkWAidYGcEIYcuXr7UdqEKXxtPiWM0xb3B91ovYvO5402bn3f9-UGtlcestxNHA"
|
||||
rClaims := &RobotClaims{}
|
||||
_, _ = ParseWithClaims(rawTk, rClaims)
|
||||
rClaims := &claim.Robot{}
|
||||
_, _ = Parse(DefaultTokenOptions(), rawTk, rClaims)
|
||||
assert.Equal(t, int64(123), rClaims.TokenID)
|
||||
assert.Equal(t, int64(0), rClaims.ProjectID)
|
||||
assert.Equal(t, "/project/libray/repository", rClaims.Access[0].Resource.String())
|
Loading…
Reference in New Issue
Block a user