mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-20 23:57:42 +01:00
Merge pull request #9506 from wy65701436/token-sevice
Enable robot account to support scan pull case
This commit is contained in:
commit
d18678a48d
@ -28,7 +28,8 @@ const (
|
|||||||
ActionDelete = Action("delete")
|
ActionDelete = Action("delete")
|
||||||
ActionList = Action("list")
|
ActionList = Action("list")
|
||||||
|
|
||||||
ActionOperate = Action("operate")
|
ActionOperate = Action("operate")
|
||||||
|
ActionScannerPull = Action("scanner-pull") // for robot account created by scanner to pull image, bypass the policy check
|
||||||
)
|
)
|
||||||
|
|
||||||
// const resource variables
|
// const resource variables
|
||||||
|
@ -47,16 +47,22 @@ func filterPolicies(namespace rbac.Namespace, policies []*rbac.Policy) []*rbac.P
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
mp := map[string]bool{}
|
mp := getAllPolicies(namespace)
|
||||||
for _, policy := range project.GetAllPolicies(namespace) {
|
|
||||||
mp[policy.String()] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, policy := range policies {
|
for _, policy := range policies {
|
||||||
if mp[policy.String()] {
|
if mp[policy.String()] {
|
||||||
results = append(results, policy)
|
results = append(results, policy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAllPolicies gets all of supported policies supported in project and external policies supported for robot account
|
||||||
|
func getAllPolicies(namespace rbac.Namespace) map[string]bool {
|
||||||
|
mp := map[string]bool{}
|
||||||
|
for _, policy := range project.GetAllPolicies(namespace) {
|
||||||
|
mp[policy.String()] = true
|
||||||
|
}
|
||||||
|
scannerPull := &rbac.Policy{Resource: namespace.Resource(rbac.ResourceRepository), Action: rbac.ActionScannerPull}
|
||||||
|
mp[scannerPull.String()] = true
|
||||||
|
return mp
|
||||||
|
}
|
||||||
|
@ -44,10 +44,11 @@ func TestGetPolicies(t *testing.T) {
|
|||||||
func TestNewRobot(t *testing.T) {
|
func TestNewRobot(t *testing.T) {
|
||||||
policies := []*rbac.Policy{
|
policies := []*rbac.Policy{
|
||||||
{Resource: "/project/1/repository", Action: "pull"},
|
{Resource: "/project/1/repository", Action: "pull"},
|
||||||
|
{Resource: "/project/1/repository", Action: "scanner-pull"},
|
||||||
{Resource: "/project/library/repository", Action: "pull"},
|
{Resource: "/project/library/repository", Action: "pull"},
|
||||||
{Resource: "/project/library/repository", Action: "push"},
|
{Resource: "/project/library/repository", Action: "push"},
|
||||||
}
|
}
|
||||||
|
|
||||||
robot := NewRobot("test", rbac.NewProjectNamespace(1, false), policies)
|
robot := NewRobot("test", rbac.NewProjectNamespace(1, false), policies)
|
||||||
assert.Len(t, robot.GetPolicies(), 1)
|
assert.Len(t, robot.GetPolicies(), 2)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -208,7 +208,7 @@ func RefreshToken(ctx context.Context, token *Token) (*Token, error) {
|
|||||||
return &Token{Token: *t, IDToken: it}, nil
|
return &Token{Token: *t, IDToken: it}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupsFromToken returns the list of group name in the token, the claim of the group list is set in OIDCSetting.
|
// GroupsFromToken returns the list of group name in the token, the claims of the group list is set in OIDCSetting.
|
||||||
// It's designed not to return errors, in case of unexpected situation it will log and return empty list.
|
// It's designed not to return errors, in case of unexpected situation it will log and return empty list.
|
||||||
func GroupsFromToken(token *gooidc.IDToken) []string {
|
func GroupsFromToken(token *gooidc.IDToken) []string {
|
||||||
if token == nil {
|
if token == nil {
|
||||||
@ -217,7 +217,7 @@ func GroupsFromToken(token *gooidc.IDToken) []string {
|
|||||||
}
|
}
|
||||||
setting := provider.setting.Load().(models.OIDCSetting)
|
setting := provider.setting.Load().(models.OIDCSetting)
|
||||||
if len(setting.GroupsClaim) == 0 {
|
if len(setting.GroupsClaim) == 0 {
|
||||||
log.Warning("Group claim is not set in OIDC setting returning empty group list.")
|
log.Warning("Group claims is not set in OIDC setting returning empty group list.")
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
var c map[string]interface{}
|
var c map[string]interface{}
|
||||||
@ -233,7 +233,7 @@ func groupsFromClaim(claimMap map[string]interface{}, k string) []string {
|
|||||||
var res []string
|
var res []string
|
||||||
g, ok := claimMap[k].([]interface{})
|
g, ok := claimMap[k].([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warningf("Unable to get groups from claims, claims: %+v, groups claim key: %s", claimMap, k)
|
log.Warningf("Unable to get groups from claims, claims: %+v, groups claims key: %s", claimMap, k)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
for _, e := range g {
|
for _, e := range g {
|
||||||
|
@ -34,7 +34,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/security/local"
|
"github.com/goharbor/harbor/src/common/security/local"
|
||||||
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
|
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
|
||||||
"github.com/goharbor/harbor/src/common/security/secret"
|
"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/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/auth"
|
"github.com/goharbor/harbor/src/core/auth"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"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/authproxy"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot"
|
"github.com/goharbor/harbor/src/pkg/robot"
|
||||||
|
pkg_token "github.com/goharbor/harbor/src/pkg/token"
|
||||||
|
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContextValueKey for content value
|
// ContextValueKey for content value
|
||||||
@ -188,15 +189,16 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||||||
if !strings.HasPrefix(robotName, common.RobotPrefix) {
|
if !strings.HasPrefix(robotName, common.RobotPrefix) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
rClaims := &token.RobotClaims{}
|
rClaims := &robot_claim.Claim{}
|
||||||
htk, err := token.ParseWithClaims(robotTk, rClaims)
|
opt := pkg_token.DefaultTokenOptions()
|
||||||
|
rtk, err := pkg_token.Parse(opt, robotTk, rClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to decrypt robot token, %v", err)
|
log.Errorf("failed to decrypt robot token, %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
|
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
|
||||||
ctr := robot.RobotCtr
|
ctr := robot.RobotCtr
|
||||||
robot, err := ctr.GetRobotAccount(htk.Claims.(*token.RobotClaims).TokenID)
|
robot, err := ctr.GetRobotAccount(rtk.Claims.(*robot_claim.Claim).TokenID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to get robot %s: %v", robotName, err)
|
log.Errorf("failed to get robot %s: %v", robotName, err)
|
||||||
return false
|
return false
|
||||||
@ -215,7 +217,7 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||||||
}
|
}
|
||||||
log.Debug("creating robot account security context...")
|
log.Debug("creating robot account security context...")
|
||||||
pm := config.GlobalProjectMgr
|
pm := config.GlobalProjectMgr
|
||||||
securCtx := robotCtx.NewSecurityContext(robot, pm, htk.Claims.(*token.RobotClaims).Access)
|
securCtx := robotCtx.NewSecurityContext(robot, pm, rtk.Claims.(*robot_claim.Claim).Access)
|
||||||
setSecurCtxAndPM(ctx.Request, securCtx, pm)
|
setSecurCtxAndPM(ctx.Request, securCtx, pm)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
|
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
|
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/readonly"
|
"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/sizequota"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/url"
|
"github.com/goharbor/harbor/src/core/middlewares/url"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/vulnerable"
|
"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) },
|
SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) },
|
||||||
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
|
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
|
||||||
IMMUTABLE: func(next http.Handler) http.Handler { return immutable.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]
|
return middlewares[mName]
|
||||||
}
|
}
|
||||||
|
@ -26,13 +26,14 @@ const (
|
|||||||
SIZEQUOTA = "sizequota"
|
SIZEQUOTA = "sizequota"
|
||||||
COUNTQUOTA = "countquota"
|
COUNTQUOTA = "countquota"
|
||||||
IMMUTABLE = "immutable"
|
IMMUTABLE = "immutable"
|
||||||
|
REGTOKEN = "regtoken"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChartMiddlewares middlewares for chart server
|
// ChartMiddlewares middlewares for chart server
|
||||||
var ChartMiddlewares = []string{CHART}
|
var ChartMiddlewares = []string{CHART}
|
||||||
|
|
||||||
// Middlewares with sequential organization
|
// 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 ...
|
// MiddlewaresLocal ...
|
||||||
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||||
|
@ -49,6 +49,10 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
|||||||
cth.next.ServeHTTP(rw, req)
|
cth.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if scannerPull, ok := util.ScannerPullFromContext(req.Context()); ok && scannerPull {
|
||||||
|
cth.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
if !util.GetPolicyChecker().ContentTrustEnabled(img.ProjectName) {
|
if !util.GetPolicyChecker().ContentTrustEnabled(img.ProjectName) {
|
||||||
cth.next.ServeHTTP(rw, req)
|
cth.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
|
71
src/core/middlewares/regtoken/handler.go
Normal file
71
src/core/middlewares/regtoken/handler.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package regtoken
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/distribution/registry/auth"
|
||||||
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
pkg_token "github.com/goharbor/harbor/src/pkg/token"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/token/claims/registry"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// regTokenHandler is responsible for decoding the registry token in the docker pull request header,
|
||||||
|
// as harbor adds customized claims action into registry auth token, the middlerware is for decode it and write it into
|
||||||
|
// request context, then for other middlerwares in chain to use it to bypass request validation.
|
||||||
|
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 {
|
||||||
|
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()
|
||||||
|
regTK, err := pkg_token.Parse(opt, rawToken, ®istry.Claim{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to decode reg token: %v, the error is skipped and round the request to native registry.", err)
|
||||||
|
r.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessItems := []auth.Access{}
|
||||||
|
accessItems = append(accessItems, auth.Access{
|
||||||
|
Resource: auth.Resource{
|
||||||
|
Type: rbac.ResourceRepository.String(),
|
||||||
|
Name: img.Repository,
|
||||||
|
},
|
||||||
|
Action: rbac.ActionScannerPull.String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
accessSet := regTK.Claims.(*registry.Claim).GetAccess()
|
||||||
|
for _, access := range accessItems {
|
||||||
|
if accessSet.Contains(access) {
|
||||||
|
*req = *(req.WithContext(util.NewScannerPullContext(req.Context(), true)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.next.ServeHTTP(rw, req)
|
||||||
|
}
|
55
src/core/middlewares/regtoken/handler_test.go
Normal file
55
src/core/middlewares/regtoken/handler_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package regtoken
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPullManifestRequest(projectName, name, tag string, next ...http.HandlerFunc) int {
|
||||||
|
repository := fmt.Sprintf("%s/%s", projectName, name)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("/v2/%s/manifests/%s", repository, tag)
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
|
||||||
|
token := "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkNWUTc6REM3NTpHVEROOkxTTUs6VUFJTjpIUUVWOlZVSDQ6Q0lRRDpRV01COlM0Qzc6U0c0STpGRUhYIn0.eyJpc3MiOiJoYXJib3ItdG9rZW4taXNzdWVyIiwic3ViIjoicm9ib3QkZGVtbzExIiwiYXVkIjoiaGFyYm9yLXJlZ2lzdHJ5IiwiZXhwIjoxNTcxNzYzOTI2LCJuYmYiOjE1NzE3NjM4NjYsImlhdCI6MTU3MTc2Mzg2NiwianRpIjoiTnRaZWx4Z01KTUU1MXlEMCIsImFjY2VzcyI6W3sidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoibGlicmFyeS9oZWxsby13b3JsZCIsImFjdGlvbnMiOlsicHVzaCIsIioiLCJwdWxsIiwic2Nhbm5lcnB1bGwiXX1dfQ.GlWuvtoxmChnpvbWaG5901Z9-g63DrzyNUREWlDbR5gnNeuOKjLNyE4QpogAQKx2yYtcGxbqNL3VfJkExJ_gMS0Qw8e10utGOawwqD4oqf_J06eKq4HzpZJengZfcjMA4g2RoeOlqdVdwimB_PdX9vkBO1od0wX0Cc2v0p2w5TkibcThKRoeLeVs2oRewkKLuVHNSM8wwRIlAvpWJuNnvRCFlHRkLcZM_KpGXqT7H-PZETTisWCi1pMxeYEwIsDFLlTKdV8LaiDeDmH-RaLOsuyAySYEW9Ynk5K3P_dUl2c_SYQXloPyi0MvXxSn6EWE4eHF2oQDM_SvIzR9sOVB8TtjMjKKMQ4yr_mqgMcfEpnInJATExBR56wmxNdLESncHl8rUYCe2jCjQFuR9NGQA1tGdjI4NoBN-OVD0dBs9rm_mkb2tgD-3gEhyzAw6hg0uzDsF7bj5Aq8scoi42UurhX2bZM89s4-TWBp4DWuBG0HDiwpOiBvB3RMm6MpQxsqrl0hQm_WH18L6QCknAW2e3d_6DJWJ0eBzISrhDr7LkqJKl1J8pv4zqoh_EUVeLyzTmjEULm-VbnpVF4wW5yTLF3S6F7Ox4vwWtVfi1XQNVOcJDB3VPUsRgiTTuCW-ZGcBLw-OdIcwaJ3T_QZkEjUw1f6i1JcGa0Mpgl83aLiSdQ 0xc0003c77c0 map[alg:RS256 kid:CVQ7:DC75:GTDN:LSMK:UAIN:HQEV:VUH4:CIQD:QWMB:S4C7:SG4I:FEHX typ:JWT] 0xc000496000 GlWuvtoxmChnpvbWaG5901Z9-g63DrzyNUREWlDbR5gnNeuOKjLNyE4QpogAQKx2yYtcGxbqNL3VfJkExJ_gMS0Qw8e10utGOawwqD4oqf_J06eKq4HzpZJengZfcjMA4g2RoeOlqdVdwimB_PdX9vkBO1od0wX0Cc2v0p2w5TkibcThKRoeLeVs2oRewkKLuVHNSM8wwRIlAvpWJuNnvRCFlHRkLcZM_KpGXqT7H-PZETTisWCi1pMxeYEwIsDFLlTKdV8LaiDeDmH-RaLOsuyAySYEW9Ynk5K3P_dUl2c_SYQXloPyi0MvXxSn6EWE4eHF2oQDM_SvIzR9sOVB8TtjMjKKMQ4yr_mqgMcfEpnInJATExBR56wmxNdLESncHl8rUYCe2jCjQFuR9NGQA1tGdjI4NoBN-OVD0dBs9rm_mkb2tgD-3gEhyzAw6hg0uzDsF7bj5Aq8scoi42UurhX2bZM89s4-TWBp4DWuBG0HDiwpOiBvB3RMm6MpQxsqrl0hQm_WH18L6QCknAW2e3d_6DJWJ0eBzISrhDr7LkqJKl1J8pv4zqoh_EUVeLyzTmjEULm-VbnpVF4wW5yTLF3S6F7Ox4vwWtVfi1XQNVOcJDB3VPUsRgiTTuCW-ZGcBLw-OdIcwaJ3T_QZkEjUw1f6i1JcGa0Mpgl83aLiSdQ"
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
var n http.HandlerFunc
|
||||||
|
if len(next) > 0 {
|
||||||
|
n = next[0]
|
||||||
|
} else {
|
||||||
|
n = func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h := New(http.HandlerFunc(n))
|
||||||
|
h.ServeHTTP(util.NewCustomResponseWriter(rr), req)
|
||||||
|
|
||||||
|
return rr.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HandlerSuite) TestPullManifest() {
|
||||||
|
code1 := doPullManifestRequest("library", "photon", "release-1.10")
|
||||||
|
suite.Equal(http.StatusNotFound, code1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
if result := m.Run(); result != 0 {
|
||||||
|
os.Exit(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHandlerSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(HandlerSuite))
|
||||||
|
}
|
@ -49,6 +49,8 @@ type contextKey string
|
|||||||
const (
|
const (
|
||||||
// ImageInfoCtxKey the context key for image information
|
// ImageInfoCtxKey the context key for image information
|
||||||
ImageInfoCtxKey = contextKey("ImageInfo")
|
ImageInfoCtxKey = contextKey("ImageInfo")
|
||||||
|
// ScannerPullCtxKey the context key for robot account to bypass the pull policy check.
|
||||||
|
ScannerPullCtxKey = contextKey("ScannerPullCheck")
|
||||||
// TokenUsername ...
|
// TokenUsername ...
|
||||||
// TODO: temp solution, remove after vmware/harbor#2242 is resolved.
|
// TODO: temp solution, remove after vmware/harbor#2242 is resolved.
|
||||||
TokenUsername = "harbor-core"
|
TokenUsername = "harbor-core"
|
||||||
@ -443,6 +445,17 @@ func ManifestInfoFromContext(ctx context.Context) (*ManifestInfo, bool) {
|
|||||||
return info, ok
|
return info, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewScannerPullContext returns context with policy check info
|
||||||
|
func NewScannerPullContext(ctx context.Context, scannerPull bool) context.Context {
|
||||||
|
return context.WithValue(ctx, ScannerPullCtxKey, scannerPull)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScannerPullFromContext returns whether to bypass policy check
|
||||||
|
func ScannerPullFromContext(ctx context.Context) (bool, bool) {
|
||||||
|
info, ok := ctx.Value(ScannerPullCtxKey).(bool)
|
||||||
|
return info, ok
|
||||||
|
}
|
||||||
|
|
||||||
// NewBlobInfoContext returns context with blob info
|
// NewBlobInfoContext returns context with blob info
|
||||||
func NewBlobInfoContext(ctx context.Context, info *BlobInfo) context.Context {
|
func NewBlobInfoContext(ctx context.Context, info *BlobInfo) context.Context {
|
||||||
return context.WithValue(ctx, blobInfoKey, info)
|
return context.WithValue(ctx, blobInfoKey, info)
|
||||||
|
@ -52,6 +52,11 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if scannerPull, ok := util.ScannerPullFromContext(req.Context()); ok && scannerPull {
|
||||||
|
vh.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Is vulnerable policy set?
|
// Is vulnerable policy set?
|
||||||
projectVulnerableEnabled, projectVulnerableSeverity, wl := util.GetPolicyChecker().VulnerablePolicy(img.ProjectName)
|
projectVulnerableEnabled, projectVulnerableSeverity, wl := util.GetPolicyChecker().VulnerablePolicy(img.ProjectName)
|
||||||
if !projectVulnerableEnabled {
|
if !projectVulnerableEnabled {
|
||||||
@ -109,10 +114,10 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print bypass CVE list
|
// Print scannerPull CVE list
|
||||||
if len(summary.CVEBypassed) > 0 {
|
if len(summary.CVEBypassed) > 0 {
|
||||||
for _, cve := range summary.CVEBypassed {
|
for _, cve := range summary.CVEBypassed {
|
||||||
log.Infof("Vulnerable policy check: bypass CVE %s", cve)
|
log.Infof("Vulnerable policy check: scannerPull CVE %s", cve)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,6 +133,9 @@ func permToActions(p string) []string {
|
|||||||
if strings.Contains(p, "R") {
|
if strings.Contains(p, "R") {
|
||||||
res = append(res, "pull")
|
res = append(res, "pull")
|
||||||
}
|
}
|
||||||
|
if strings.Contains(p, "S") {
|
||||||
|
res = append(res, "scanner-pull")
|
||||||
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +177,8 @@ func (rep repositoryFilter) filter(ctx security.Context, pm promgr.ProjectManage
|
|||||||
permission = "RWM"
|
permission = "RWM"
|
||||||
} else if ctx.Can(rbac.ActionPush, resource) {
|
} else if ctx.Can(rbac.ActionPush, resource) {
|
||||||
permission = "RW"
|
permission = "RW"
|
||||||
|
} else if ctx.Can(rbac.ActionScannerPull, resource) {
|
||||||
|
permission = "RS"
|
||||||
} else if ctx.Can(rbac.ActionPull, resource) {
|
} else if ctx.Can(rbac.ActionPull, resource) {
|
||||||
permission = "R"
|
permission = "R"
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,14 @@ package robot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
"github.com/goharbor/harbor/src/common"
|
"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/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/token"
|
||||||
|
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -76,13 +78,23 @@ func (d *DefaultAPIController) CreateRobotAccount(robotReq *model.RobotCreate) (
|
|||||||
|
|
||||||
// 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, robotReq.ProjectID, expiresAt, robotReq.Access)
|
opt := token.DefaultTokenOptions()
|
||||||
|
rClaims := &robot_claim.Claim{
|
||||||
|
TokenID: id,
|
||||||
|
ProjectID: robotReq.ProjectID,
|
||||||
|
Access: robotReq.Access,
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
IssuedAt: time.Now().UTC().Unix(),
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
Issuer: opt.Issuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tk, err := token.New(opt, rClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deferDel = err
|
deferDel = err
|
||||||
return nil, fmt.Errorf("failed to valid parameters to generate token for robot account, %v", err)
|
return nil, fmt.Errorf("failed to valid parameters to generate token for robot account, %v", err)
|
||||||
}
|
}
|
||||||
|
rawTk, err := tk.Raw()
|
||||||
rawTk, err := jwtToken.Raw()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deferDel = err
|
deferDel = err
|
||||||
return nil, fmt.Errorf("failed to sign token for robot account, %v", err)
|
return nil, fmt.Errorf("failed to sign token for robot account, %v", err)
|
||||||
|
49
src/pkg/token/claims/registry/accesses.go
Normal file
49
src/pkg/token/claims/registry/accesses.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/distribution/registry/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Accesses ...
|
||||||
|
type Accesses map[auth.Resource]actions
|
||||||
|
|
||||||
|
// Contains ...
|
||||||
|
func (s Accesses) Contains(access auth.Access) bool {
|
||||||
|
actionSet, ok := s[access.Resource]
|
||||||
|
if ok {
|
||||||
|
return actionSet.contains(access.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type actions struct {
|
||||||
|
stringSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActions(set ...string) actions {
|
||||||
|
return actions{newStringSet(set...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s actions) contains(action string) bool {
|
||||||
|
return s.stringSet.contains(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringSet map[string]struct{}
|
||||||
|
|
||||||
|
func newStringSet(keys ...string) stringSet {
|
||||||
|
ss := make(stringSet, len(keys))
|
||||||
|
ss.add(keys...)
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss stringSet) add(keys ...string) {
|
||||||
|
for _, key := range keys {
|
||||||
|
ss[key] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss stringSet) contains(key string) bool {
|
||||||
|
_, ok := ss[key]
|
||||||
|
return ok
|
||||||
|
}
|
38
src/pkg/token/claims/registry/registry.go
Normal file
38
src/pkg/token/claims/registry/registry.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/docker/distribution/registry/auth"
|
||||||
|
"github.com/docker/distribution/registry/auth/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Claim implements the interface of jwt.Claims
|
||||||
|
type Claim struct {
|
||||||
|
jwt.StandardClaims
|
||||||
|
Access []*token.ResourceActions `json:"access"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid valid the standard claims
|
||||||
|
func (rc *Claim) Valid() error {
|
||||||
|
return rc.StandardClaims.Valid()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccess ...
|
||||||
|
func (rc *Claim) GetAccess() Accesses {
|
||||||
|
accesses := make(Accesses, len(rc.Access))
|
||||||
|
for _, resourceActions := range rc.Access {
|
||||||
|
resource := auth.Resource{
|
||||||
|
Type: resourceActions.Type,
|
||||||
|
Name: resourceActions.Name,
|
||||||
|
}
|
||||||
|
set, exists := accesses[resource]
|
||||||
|
if !exists {
|
||||||
|
set = newActions()
|
||||||
|
accesses[resource] = set
|
||||||
|
}
|
||||||
|
for _, action := range resourceActions.Actions {
|
||||||
|
set.add(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accesses
|
||||||
|
}
|
54
src/pkg/token/claims/registry/registry_test.go
Normal file
54
src/pkg/token/claims/registry/registry_test.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/distribution/registry/auth"
|
||||||
|
"github.com/docker/distribution/registry/auth/token"
|
||||||
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValid(t *testing.T) {
|
||||||
|
access := &token.ResourceActions{
|
||||||
|
Type: "type",
|
||||||
|
Name: "repository",
|
||||||
|
Actions: []string{"pull", "push"},
|
||||||
|
}
|
||||||
|
accesses := []*token.ResourceActions{}
|
||||||
|
accesses = append(accesses, access)
|
||||||
|
rClaims := &Claim{
|
||||||
|
Access: accesses,
|
||||||
|
}
|
||||||
|
assert.Nil(t, rClaims.Valid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAccessSet(t *testing.T) {
|
||||||
|
access := &token.ResourceActions{
|
||||||
|
Type: "repository",
|
||||||
|
Name: "hello-world",
|
||||||
|
Actions: []string{"pull", "push", "scanner-pull"},
|
||||||
|
}
|
||||||
|
accesses := []*token.ResourceActions{}
|
||||||
|
accesses = append(accesses, access)
|
||||||
|
rClaims := &Claim{
|
||||||
|
Access: accesses,
|
||||||
|
}
|
||||||
|
|
||||||
|
auth1 := auth.Access{
|
||||||
|
Resource: auth.Resource{
|
||||||
|
Type: "repository",
|
||||||
|
Name: "hello-world",
|
||||||
|
},
|
||||||
|
Action: rbac.ActionScannerPull.String(),
|
||||||
|
}
|
||||||
|
auth2 := auth.Access{
|
||||||
|
Resource: auth.Resource{
|
||||||
|
Type: "repository",
|
||||||
|
Name: "busubox",
|
||||||
|
},
|
||||||
|
Action: rbac.ActionScannerPull.String(),
|
||||||
|
}
|
||||||
|
set := rClaims.GetAccess()
|
||||||
|
assert.True(t, set.Contains(auth1))
|
||||||
|
assert.False(t, set.Contains(auth2))
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package token
|
package robot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -6,8 +6,8 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RobotClaims implements the interface of jwt.Claims
|
// Claim implements the interface of jwt.Claims
|
||||||
type RobotClaims struct {
|
type Claim struct {
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
TokenID int64 `json:"id"`
|
TokenID int64 `json:"id"`
|
||||||
ProjectID int64 `json:"pid"`
|
ProjectID int64 `json:"pid"`
|
||||||
@ -15,7 +15,7 @@ type RobotClaims struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Valid valid the claims "tokenID, projectID and access".
|
// Valid valid the claims "tokenID, projectID and access".
|
||||||
func (rc RobotClaims) Valid() error {
|
func (rc Claim) Valid() error {
|
||||||
if rc.TokenID < 0 {
|
if rc.TokenID < 0 {
|
||||||
return errors.New("Token id must an valid INT")
|
return errors.New("Token id must an valid INT")
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package token
|
package robot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
@ -15,7 +15,7 @@ func TestValid(t *testing.T) {
|
|||||||
policies := []*rbac.Policy{}
|
policies := []*rbac.Policy{}
|
||||||
policies = append(policies, rbacPolicy)
|
policies = append(policies, rbacPolicy)
|
||||||
|
|
||||||
rClaims := &RobotClaims{
|
rClaims := &Claim{
|
||||||
TokenID: 1,
|
TokenID: 1,
|
||||||
ProjectID: 2,
|
ProjectID: 2,
|
||||||
Access: policies,
|
Access: policies,
|
||||||
@ -32,7 +32,7 @@ func TestUnValidTokenID(t *testing.T) {
|
|||||||
policies := []*rbac.Policy{}
|
policies := []*rbac.Policy{}
|
||||||
policies = append(policies, rbacPolicy)
|
policies = append(policies, rbacPolicy)
|
||||||
|
|
||||||
rClaims := &RobotClaims{
|
rClaims := &Claim{
|
||||||
TokenID: -1,
|
TokenID: -1,
|
||||||
ProjectID: 2,
|
ProjectID: 2,
|
||||||
Access: policies,
|
Access: policies,
|
||||||
@ -49,7 +49,7 @@ func TestUnValidProjectID(t *testing.T) {
|
|||||||
policies := []*rbac.Policy{}
|
policies := []*rbac.Policy{}
|
||||||
policies = append(policies, rbacPolicy)
|
policies = append(policies, rbacPolicy)
|
||||||
|
|
||||||
rClaims := &RobotClaims{
|
rClaims := &Claim{
|
||||||
TokenID: 1,
|
TokenID: 1,
|
||||||
ProjectID: -2,
|
ProjectID: -2,
|
||||||
Access: policies,
|
Access: policies,
|
||||||
@ -59,7 +59,7 @@ func TestUnValidProjectID(t *testing.T) {
|
|||||||
|
|
||||||
func TestUnValidPolicy(t *testing.T) {
|
func TestUnValidPolicy(t *testing.T) {
|
||||||
|
|
||||||
rClaims := &RobotClaims{
|
rClaims := &Claim{
|
||||||
TokenID: 1,
|
TokenID: 1,
|
||||||
ProjectID: 2,
|
ProjectID: 2,
|
||||||
Access: nil,
|
Access: nil,
|
@ -8,15 +8,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewOptions(t *testing.T) {
|
func TestNewOptions(t *testing.T) {
|
||||||
defaultOpt := DefaultOptions()
|
defaultOpt := DefaultTokenOptions()
|
||||||
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-defaultIssuer")
|
||||||
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
|
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetKey(t *testing.T) {
|
func TestGetKey(t *testing.T) {
|
||||||
defaultOpt := DefaultOptions()
|
defaultOpt := DefaultTokenOptions()
|
||||||
key, err := defaultOpt.GetKey()
|
key, err := defaultOpt.GetKey()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, key)
|
assert.NotNil(t, key)
|
@ -11,9 +11,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ttl = 60 * time.Minute
|
defaultTTL = 60 * time.Minute
|
||||||
issuer = "harbor-token-issuer"
|
defaultIssuer = "harbor-token-defaultIssuer"
|
||||||
signedMethod = "RS256"
|
defaultSignedMethod = "RS256"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options ...
|
// Options ...
|
||||||
@ -25,23 +25,6 @@ type Options struct {
|
|||||||
Issuer string
|
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 ...
|
// GetKey ...
|
||||||
func (o *Options) GetKey() (interface{}, error) {
|
func (o *Options) GetKey() (interface{}, error) {
|
||||||
var err 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))
|
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"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
"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"
|
||||||
|
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +35,15 @@ func TestNew(t *testing.T) {
|
|||||||
projectID := int64(321)
|
projectID := int64(321)
|
||||||
tokenExpiration := time.Duration(10) * 24 * time.Hour
|
tokenExpiration := time.Duration(10) * 24 * time.Hour
|
||||||
expiresAt := time.Now().UTC().Add(tokenExpiration).Unix()
|
expiresAt := time.Now().UTC().Add(tokenExpiration).Unix()
|
||||||
token, err := New(tokenID, projectID, expiresAt, policies)
|
robot := robot_claim.Claim{
|
||||||
|
TokenID: tokenID,
|
||||||
|
ProjectID: projectID,
|
||||||
|
Access: policies,
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token, err := New(DefaultTokenOptions(), robot)
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, token.Header["alg"], "RS256")
|
assert.Equal(t, token.Header["alg"], "RS256")
|
||||||
@ -54,7 +64,15 @@ func TestRaw(t *testing.T) {
|
|||||||
|
|
||||||
tokenExpiration := time.Duration(10) * 24 * time.Hour
|
tokenExpiration := time.Duration(10) * 24 * time.Hour
|
||||||
expiresAt := time.Now().UTC().Add(tokenExpiration).Unix()
|
expiresAt := time.Now().UTC().Add(tokenExpiration).Unix()
|
||||||
token, err := New(tokenID, projectID, expiresAt, policies)
|
robot := robot_claim.Claim{
|
||||||
|
TokenID: tokenID,
|
||||||
|
ProjectID: projectID,
|
||||||
|
Access: policies,
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token, err := New(DefaultTokenOptions(), robot)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
rawTk, err := token.Raw()
|
rawTk, err := token.Raw()
|
||||||
@ -64,8 +82,8 @@ func TestRaw(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseWithClaims(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"
|
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{}
|
rClaims := &robot_claim.Claim{}
|
||||||
_, _ = ParseWithClaims(rawTk, rClaims)
|
_, _ = Parse(DefaultTokenOptions(), rawTk, rClaims)
|
||||||
assert.Equal(t, int64(123), rClaims.TokenID)
|
assert.Equal(t, int64(123), rClaims.TokenID)
|
||||||
assert.Equal(t, int64(0), rClaims.ProjectID)
|
assert.Equal(t, int64(0), rClaims.ProjectID)
|
||||||
assert.Equal(t, "/project/libray/repository", rClaims.Access[0].Resource.String())
|
assert.Equal(t, "/project/libray/repository", rClaims.Access[0].Resource.String())
|
Loading…
Reference in New Issue
Block a user