Merge pull request #9506 from wy65701436/token-sevice

Enable robot account to support scan pull case
This commit is contained in:
Wang Yan 2019-10-24 19:52:33 +08:00 committed by GitHub
commit d18678a48d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 473 additions and 146 deletions

View File

@ -29,6 +29,7 @@ const (
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 &regTokenHandler{
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, &registry.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)
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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