mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 09:38:09 +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
@ -29,6 +29,7 @@ const (
|
||||
ActionList = Action("list")
|
||||
|
||||
ActionOperate = Action("operate")
|
||||
ActionScannerPull = Action("scanner-pull") // for robot account created by scanner to pull image, bypass the policy check
|
||||
)
|
||||
|
||||
// const resource variables
|
||||
|
@ -47,16 +47,22 @@ func filterPolicies(namespace rbac.Namespace, policies []*rbac.Policy) []*rbac.P
|
||||
return results
|
||||
}
|
||||
|
||||
mp := map[string]bool{}
|
||||
for _, policy := range project.GetAllPolicies(namespace) {
|
||||
mp[policy.String()] = true
|
||||
}
|
||||
|
||||
mp := getAllPolicies(namespace)
|
||||
for _, policy := range policies {
|
||||
if mp[policy.String()] {
|
||||
results = append(results, policy)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
policies := []*rbac.Policy{
|
||||
{Resource: "/project/1/repository", Action: "pull"},
|
||||
{Resource: "/project/1/repository", Action: "scanner-pull"},
|
||||
{Resource: "/project/library/repository", Action: "pull"},
|
||||
{Resource: "/project/library/repository", Action: "push"},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func GroupsFromToken(token *gooidc.IDToken) []string {
|
||||
if token == nil {
|
||||
@ -217,7 +217,7 @@ func GroupsFromToken(token *gooidc.IDToken) []string {
|
||||
}
|
||||
setting := provider.setting.Load().(models.OIDCSetting)
|
||||
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{}
|
||||
}
|
||||
var c map[string]interface{}
|
||||
@ -233,7 +233,7 @@ func groupsFromClaim(claimMap map[string]interface{}, k string) []string {
|
||||
var res []string
|
||||
g, ok := claimMap[k].([]interface{})
|
||||
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
|
||||
}
|
||||
for _, e := range g {
|
||||
|
@ -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"
|
||||
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||
)
|
||||
|
||||
// ContextValueKey for content value
|
||||
@ -188,15 +189,16 @@ 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 := &robot_claim.Claim{}
|
||||
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.(*robot_claim.Claim).TokenID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get robot %s: %v", robotName, err)
|
||||
return false
|
||||
@ -215,7 +217,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.(*robot_claim.Claim).Access)
|
||||
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,10 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
if scannerPull, ok := util.ScannerPullFromContext(req.Context()); ok && scannerPull {
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
if !util.GetPolicyChecker().ContentTrustEnabled(img.ProjectName) {
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
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 (
|
||||
// ImageInfoCtxKey the context key for image information
|
||||
ImageInfoCtxKey = contextKey("ImageInfo")
|
||||
// ScannerPullCtxKey the context key for robot account to bypass the pull policy check.
|
||||
ScannerPullCtxKey = contextKey("ScannerPullCheck")
|
||||
// TokenUsername ...
|
||||
// TODO: temp solution, remove after vmware/harbor#2242 is resolved.
|
||||
TokenUsername = "harbor-core"
|
||||
@ -443,6 +445,17 @@ func ManifestInfoFromContext(ctx context.Context) (*ManifestInfo, bool) {
|
||||
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
|
||||
func NewBlobInfoContext(ctx context.Context, info *BlobInfo) context.Context {
|
||||
return context.WithValue(ctx, blobInfoKey, info)
|
||||
|
@ -52,6 +52,11 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if scannerPull, ok := util.ScannerPullFromContext(req.Context()); ok && scannerPull {
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Is vulnerable policy set?
|
||||
projectVulnerableEnabled, projectVulnerableSeverity, wl := util.GetPolicyChecker().VulnerablePolicy(img.ProjectName)
|
||||
if !projectVulnerableEnabled {
|
||||
@ -109,10 +114,10 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Print bypass CVE list
|
||||
// Print scannerPull CVE list
|
||||
if len(summary.CVEBypassed) > 0 {
|
||||
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") {
|
||||
res = append(res, "pull")
|
||||
}
|
||||
if strings.Contains(p, "S") {
|
||||
res = append(res, "scanner-pull")
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -177,6 +177,8 @@ func (rep repositoryFilter) filter(ctx security.Context, pm promgr.ProjectManage
|
||||
permission = "RWM"
|
||||
} else if ctx.Can(rbac.ActionPush, resource) {
|
||||
permission = "RW"
|
||||
} else if ctx.Can(rbac.ActionScannerPull, resource) {
|
||||
permission = "RS"
|
||||
} else if ctx.Can(rbac.ActionPull, resource) {
|
||||
permission = "R"
|
||||
}
|
||||
|
@ -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"
|
||||
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
@ -76,13 +78,23 @@ 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 := &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 {
|
||||
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)
|
||||
|
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 (
|
||||
"errors"
|
||||
@ -6,8 +6,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
)
|
||||
|
||||
// RobotClaims implements the interface of jwt.Claims
|
||||
type RobotClaims struct {
|
||||
// Claim implements the interface of jwt.Claims
|
||||
type Claim struct {
|
||||
jwt.StandardClaims
|
||||
TokenID int64 `json:"id"`
|
||||
ProjectID int64 `json:"pid"`
|
||||
@ -15,7 +15,7 @@ type RobotClaims struct {
|
||||
}
|
||||
|
||||
// Valid valid the claims "tokenID, projectID and access".
|
||||
func (rc RobotClaims) Valid() error {
|
||||
func (rc Claim) Valid() error {
|
||||
if rc.TokenID < 0 {
|
||||
return errors.New("Token id must an valid INT")
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package token
|
||||
package robot
|
||||
|
||||
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 := &Claim{
|
||||
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 := &Claim{
|
||||
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 := &Claim{
|
||||
TokenID: 1,
|
||||
ProjectID: -2,
|
||||
Access: policies,
|
||||
@ -59,7 +59,7 @@ func TestUnValidProjectID(t *testing.T) {
|
||||
|
||||
func TestUnValidPolicy(t *testing.T) {
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
rClaims := &Claim{
|
||||
TokenID: 1,
|
||||
ProjectID: 2,
|
||||
Access: nil,
|
@ -8,15 +8,15 @@ 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")
|
||||
assert.Equal(t, defaultOpt.Issuer, "harbor-token-defaultIssuer")
|
||||
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
|
||||
}
|
||||
|
||||
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"
|
||||
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||
"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 := robot_claim.Claim{
|
||||
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 := robot_claim.Claim{
|
||||
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 := &robot_claim.Claim{}
|
||||
_, _ = 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