Merge pull request #2878 from ywk253100/170724_registry

Refactor registry client
This commit is contained in:
Wenkai Yin 2017-07-28 13:40:32 +08:00 committed by GitHub
commit ce169e74dc
24 changed files with 647 additions and 619 deletions

View File

@ -17,18 +17,20 @@ package notary
import (
"encoding/hex"
"fmt"
"net/http"
"os"
"path"
"strings"
"github.com/docker/distribution/registry/auth/token"
"github.com/docker/notary"
"github.com/docker/notary/client"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/ui/config"
tokenutil "github.com/vmware/harbor/src/ui/service/token"
"github.com/opencontainers/go-digest"
)
@ -71,15 +73,23 @@ func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]
// GetTargets is a help function called by API to fetch signature information of a given repository.
// Per docker's convention the repository should contain the information of endpoint, i.e. it should look
// like "10.117.4.117/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo)
// like "192.168.0.1/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo)
func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]Target, error) {
res := []Target{}
authorizer := auth.NewNotaryUsernameTokenAuthorizer(username, "repository", fqRepo, "pull")
store, err := auth.NewAuthorizerStore(strings.Split(notaryEndpoint, "//")[1], true, authorizer)
t, err := tokenutil.MakeToken(username, tokenutil.Notary,
[]*token.ResourceActions{
&token.ResourceActions{
Type: "repository",
Name: fqRepo,
Actions: []string{"pull"},
}})
if err != nil {
return res, err
return nil, err
}
tr := registry.NewTransport(registry.GetHTTPTransport(true), store)
authorizer := &notaryAuthorizer{
token: t.Token,
}
tr := registry.NewTransport(registry.GetHTTPTransport(true), authorizer)
gun := data.GUN(fqRepo)
notaryRepo, err := client.NewFileCachedNotaryRepository(notaryCachePath, gun, notaryEndpoint, tr, mockRetriever, trustPin)
if err != nil {
@ -112,3 +122,13 @@ func DigestFromTarget(t Target) (string, error) {
}
return digest.NewDigestFromHex("sha256", hex.EncodeToString(sha)).String(), nil
}
type notaryAuthorizer struct {
token string
}
func (n *notaryAuthorizer) Modify(req *http.Request) error {
req.Header.Add(http.CanonicalHeaderKey("Authorization"),
fmt.Sprintf("Bearer %s", n.token))
return nil
}

View File

@ -16,6 +16,7 @@ package notary
import (
"encoding/json"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common"
notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
@ -36,9 +37,10 @@ func TestMain(m *testing.M) {
notaryServer = notarytest.NewNotaryServer(endpoint)
defer notaryServer.Close()
var defaultConfig = map[string]interface{}{
common.ExtEndpoint: "https://" + endpoint,
common.WithNotary: true,
common.CfgExpiration: 5,
common.ExtEndpoint: "https://" + endpoint,
common.WithNotary: true,
common.CfgExpiration: 5,
common.TokenExpiration: 30,
}
adminServer, err := utilstest.NewAdminserver(defaultConfig)
if err != nil {

View File

@ -1,107 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/registry"
)
// Authorizer authorizes requests according to the schema
type Authorizer interface {
// Scheme : basic, bearer
Scheme() string
//Authorize adds basic auth or token auth to the header of request
Authorize(req *http.Request, params map[string]string) error
}
// AuthorizerStore holds a authorizer list, which will authorize request.
// And it implements interface Modifier
type AuthorizerStore struct {
authorizers []Authorizer
ping *url.URL
challenges []challenge.Challenge
}
// NewAuthorizerStore ...
func NewAuthorizerStore(endpoint string, insecure bool, authorizers ...Authorizer) (*AuthorizerStore, error) {
endpoint = utils.FormatEndpoint(endpoint)
client := &http.Client{
Transport: registry.GetHTTPTransport(insecure),
Timeout: 30 * time.Second,
}
pingURL := buildPingURL(endpoint)
resp, err := client.Get(pingURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
challenges := ParseChallengeFromResponse(resp)
ping, err := url.Parse(pingURL)
if err != nil {
return nil, err
}
return &AuthorizerStore{
authorizers: authorizers,
ping: ping,
challenges: challenges,
}, nil
}
func buildPingURL(endpoint string) string {
return fmt.Sprintf("%s/v2/", endpoint)
}
// Modify adds authorization to the request
func (a *AuthorizerStore) Modify(req *http.Request) error {
//only handle the requests sent to registry
v2Index := strings.Index(req.URL.Path, "/v2/")
if v2Index == -1 {
return nil
}
ping := url.URL{
Host: req.URL.Host,
Scheme: req.URL.Scheme,
Path: req.URL.Path[:v2Index+4],
}
if ping.Host != a.ping.Host || ping.Scheme != a.ping.Scheme ||
ping.Path != a.ping.Path {
return nil
}
for _, challenge := range a.challenges {
for _, authorizer := range a.authorizers {
if authorizer.Scheme() == challenge.Scheme {
if err := authorizer.Authorize(req, challenge.Parameters); err != nil {
return err
}
}
}
}
return nil
}

View File

@ -1,108 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"net/http"
"net/url"
"strings"
"testing"
ch "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/vmware/harbor/src/common/utils/test"
)
func TestNewAuthorizerStore(t *testing.T) {
handler := test.Handler(&test.Response{
StatusCode: http.StatusUnauthorized,
Headers: map[string]string{
"Www-Authenticate": "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"",
},
})
server := test.NewServer(&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/v2/",
Handler: handler,
})
defer server.Close()
_, err := NewAuthorizerStore(server.URL, false, nil)
if err != nil {
t.Fatalf("failed to create authorizer store: %v", err)
}
}
type simpleAuthorizer struct {
}
func (s *simpleAuthorizer) Scheme() string {
return "bearer"
}
func (s *simpleAuthorizer) Authorize(req *http.Request,
params map[string]string) error {
req.Header.Set("Authorization", "Bearer token")
return nil
}
func TestModify(t *testing.T) {
authorizer := &simpleAuthorizer{}
challenge := ch.Challenge{
Scheme: "bearer",
}
ping, err := url.Parse("http://example.com/v2/")
if err != nil {
t.Fatalf("failed to parse URL: %v", err)
}
as := &AuthorizerStore{
authorizers: []Authorizer{authorizer},
ping: ping,
challenges: []ch.Challenge{challenge},
}
req, err := http.NewRequest("GET", "http://example.com/v2/ubuntu/manifests/14.04", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if err = as.Modify(req); err != nil {
t.Fatalf("failed to modify request: %v", err)
}
header := req.Header.Get("Authorization")
if len(header) == 0 {
t.Fatal("\"Authorization\" header not found")
}
if !strings.HasPrefix(header, "Bearer") {
t.Fatal("\"Authorization\" header does not start with \"Bearer\"")
}
req, err = http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if err = as.Modify(req); err != nil {
t.Fatalf("failed to modify request: %v", err)
}
header = req.Header.Get("Authorization")
if len(header) != 0 {
t.Fatal("\"Authorization\" header should not be added")
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"regexp"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
"github.com/vmware/harbor/src/common/utils/log"
)
var (
base = regexp.MustCompile("/v2")
catalog = regexp.MustCompile("/v2/_catalog")
tag = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/tags/list")
manifest = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")")
blob = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/blobs/" + digest.DigestRegexp.String())
blobUpload = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/blobs/uploads")
blobUploadChunk = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/blobs/uploads/[a-zA-Z0-9-_.=]+")
repoRegExps = []*regexp.Regexp{tag, manifest, blob, blobUploadChunk, blobUpload}
)
// parse the repository name from path, if the path doesn't match any
// regular expressions in repoRegExps, nil string will be returned
func parseRepository(path string) string {
for _, regExp := range repoRegExps {
subs := regExp.FindStringSubmatch(path)
// no match
if subs == nil {
continue
}
// match
// the subs should contain at least 2 matching texts, the first one matches
// the whole regular expression, and the second one matches the repository
// part
if len(subs) < 2 {
log.Warningf("unexpected length of sub matches: %d, should >= 2 ", len(subs))
continue
}
return subs[1]
}
return ""
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseRepository(t *testing.T) {
cases := []struct {
input string
output string
}{
{"/v2", ""},
{"/v2/_catalog", ""},
{"/v2/library/tags/list", "library"},
{"/v2/tags/list", ""},
{"/v2/tags/list/tags/list", "tags/list"},
{"/v2/library/manifests/latest", "library"},
{"/v2/library/manifests/sha256:1234567890", "library"},
{"/v2/library/blobs/sha256:1234567890", "library"},
{"/v2/library/blobs/uploads", "library"},
{"/v2/library/blobs/uploads/1234567890", "library"},
}
for _, c := range cases {
assert.Equal(t, c.output, parseRepository(c.input))
}
}

View File

@ -17,232 +17,326 @@ package auth
import (
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/docker/distribution/registry/auth/token"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
token_util "github.com/vmware/harbor/src/ui/service/token"
)
const (
latency int = 10 //second, the network latency when token is received
scheme = "bearer"
)
// Scope ...
type Scope struct {
Type string
Name string
Actions []string
type tokenGenerator interface {
generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error)
}
func (s *Scope) string() string {
return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
}
type tokenGenerator func(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error)
// Implements interface Authorizer
// tokenAuthorizer implements registry.Modifier interface. It parses scopses
// from the request, generates authentication token and modifies the requset
// by adding the token
type tokenAuthorizer struct {
scope *Scope
tg tokenGenerator
cache string // cached token
expiresAt *time.Time // The UTC standard time at when the token will expire
registryURL *url.URL // used to filter request
generator tokenGenerator
client *http.Client
cachedTokens map[string]*models.Token
sync.Mutex
}
// Scheme returns the scheme that the handler can handle
func (t *tokenAuthorizer) Scheme() string {
return "bearer"
}
// add token to the request
func (t *tokenAuthorizer) Modify(req *http.Request) error {
//only handle requests sent to registry
goon, err := t.filterReq(req)
if err != nil {
return err
}
// AuthorizeRequest will add authorization header which contains a token before the request is sent
func (t *tokenAuthorizer) Authorize(req *http.Request, params map[string]string) error {
var scopes []*Scope
var token string
if !goon {
log.Debugf("the request %s is not sent to registry, skip", req.URL.String())
return nil
}
hasFrom := false
from := req.URL.Query().Get("from")
if len(from) != 0 {
s := &Scope{
Type: "repository",
Name: from,
Actions: []string{"pull"},
// parse scopes from request
scopes, err := parseScopes(req)
if err != nil {
return err
}
var token *models.Token
// try to get token from cache if the request is for empty scope(login)
// or single scope
if len(scopes) <= 1 {
key := ""
if len(scopes) == 1 {
key = scopeString(scopes[0])
}
scopes = append(scopes, s)
// do not cache the token if "from" appears
hasFrom = true
token = t.getCachedToken(key)
}
if t.scope != nil {
scopes = append(scopes, t.scope)
}
expired := true
cachedToken, cachedExpiredAt := t.getCachedToken()
if len(cachedToken) != 0 && cachedExpiredAt != nil {
expired = cachedExpiredAt.Before(time.Now().UTC())
}
if expired || hasFrom {
scopeStrs := []string{}
for _, scope := range scopes {
scopeStrs = append(scopeStrs, scope.string())
}
to, expiresIn, _, err := t.tg(params["realm"], params["service"], scopeStrs)
// request a new token if the token is null
if token == nil {
token, err = t.generator.generate(scopes, t.registryURL.String())
if err != nil {
return err
}
token = to
if !hasFrom {
t.updateCachedToken(to, expiresIn)
// if the token is null(this happens if the registry needs no authentication), return
// directly. Or the token will be cached
if token == nil {
return nil
}
// only cache the token for empty scope(login) or single scope request
if len(scopes) <= 1 {
key := ""
if len(scopes) == 1 {
key = scopeString(scopes[0])
}
t.updateCachedToken(key, token)
}
} else {
token = cachedToken
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token))
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token.Token))
return nil
}
func (t *tokenAuthorizer) getCachedToken() (string, *time.Time) {
t.Lock()
defer t.Unlock()
return t.cache, t.expiresAt
func scopeString(scope *token.ResourceActions) string {
if scope == nil {
return ""
}
return fmt.Sprintf("%s:%s:%s", scope.Type, scope.Name, strings.Join(scope.Actions, ","))
}
func (t *tokenAuthorizer) updateCachedToken(token string, expiresIn int) {
t.Lock()
defer t.Unlock()
t.cache = token
n := (time.Duration)(expiresIn - latency)
e := time.Now().Add(n * time.Second).UTC()
t.expiresAt = &e
// some requests are sent to backend storage, such as s3, this method filters
// the requests only sent to registry
func (t *tokenAuthorizer) filterReq(req *http.Request) (bool, error) {
// the registryURL is nil when the first request comes, init it with
// the scheme and host of the request which must be sent to the registry
if t.registryURL == nil {
u, err := url.Parse(buildPingURL(req.URL.Scheme + "://" + req.URL.Host))
if err != nil {
return false, err
}
t.registryURL = u
}
v2Index := strings.Index(req.URL.Path, "/v2/")
if v2Index == -1 {
return false, nil
}
if req.URL.Host != t.registryURL.Host || req.URL.Scheme != t.registryURL.Scheme ||
req.URL.Path[:v2Index+4] != t.registryURL.Path {
return false, nil
}
return true, nil
}
// Implements interface Authorizer
type standardTokenAuthorizer struct {
tokenAuthorizer
client *http.Client
credential Credential
tokenServiceEndpoint string
// parse scopes from the request according to its method, path and query string
func parseScopes(req *http.Request) ([]*token.ResourceActions, error) {
scopes := []*token.ResourceActions{}
from := req.URL.Query().Get("from")
if len(from) != 0 {
scopes = append(scopes, &token.ResourceActions{
Type: "repository",
Name: from,
Actions: []string{"pull"},
})
}
var scope *token.ResourceActions
path := strings.TrimRight(req.URL.Path, "/")
repository := parseRepository(path)
if len(repository) > 0 {
// pull, push, delete blob/manifest
scope = &token.ResourceActions{
Type: "repository",
Name: repository,
}
switch req.Method {
case http.MethodGet, http.MethodHead:
scope.Actions = []string{"pull"}
case http.MethodPost, http.MethodPut, http.MethodPatch:
scope.Actions = []string{"push"}
case http.MethodDelete:
scope.Actions = []string{"*"}
default:
scope = nil
log.Warningf("unsupported method: %s", req.Method)
}
} else if catalog.MatchString(path) {
// catalog
scope = &token.ResourceActions{
Type: "registry",
Name: "catalog",
Actions: []string{"*"},
}
} else if base.MatchString(path) {
// base
scope = nil
} else {
// unknow
return scopes, fmt.Errorf("can not parse scope from the request: %s %s", req.Method, req.URL.Path)
}
if scope != nil {
scopes = append(scopes, scope)
}
strs := []string{}
for _, s := range scopes {
strs = append(strs, scopeString(s))
}
log.Debugf("scopses parsed from request: %s", strings.Join(strs, " "))
return scopes, nil
}
func (t *tokenAuthorizer) getCachedToken(scope string) *models.Token {
t.Lock()
defer t.Unlock()
token := t.cachedTokens[scope]
if token == nil {
return nil
}
issueAt, err := time.Parse(time.RFC3339, token.IssuedAt)
if err != nil {
log.Errorf("failed parse %s: %v", token.IssuedAt, err)
delete(t.cachedTokens, scope)
return nil
}
if issueAt.Add(time.Duration(token.ExpiresIn-latency) * time.Second).Before(time.Now().UTC()) {
delete(t.cachedTokens, scope)
return nil
}
log.Debugf("get token for scope %s from cache", scope)
return token
}
func (t *tokenAuthorizer) updateCachedToken(scope string, token *models.Token) {
t.Lock()
defer t.Unlock()
t.cachedTokens[scope] = token
}
// ping returns the realm, service and error
func ping(client *http.Client, endpoint string) (string, string, error) {
resp, err := client.Get(endpoint)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
challenges := ParseChallengeFromResponse(resp)
for _, challenge := range challenges {
if scheme == challenge.Scheme {
realm := challenge.Parameters["realm"]
service := challenge.Parameters["service"]
return realm, service, nil
}
}
log.Warningf("schemes %v are unsupportted", challenges)
return "", "", nil
}
// NewStandardTokenAuthorizer returns a standard token authorizer. The authorizer will request a token
// from token server and add it to the origin request
// If tokenServiceEndpoint is set, the token request will be sent to it instead of the server get from authorizer
// The usage please refer to the function tokenURL
// If customizedTokenService is set, the token request will be sent to it instead of the server get from authorizer
func NewStandardTokenAuthorizer(credential Credential, insecure bool,
tokenServiceEndpoint string, scopeType, scopeName string, scopeActions ...string) Authorizer {
authorizer := &standardTokenAuthorizer{
client: &http.Client{
Transport: registry.GetHTTPTransport(insecure),
Timeout: 30 * time.Second,
},
credential: credential,
tokenServiceEndpoint: tokenServiceEndpoint,
customizedTokenService ...string) registry.Modifier {
client := &http.Client{
Transport: registry.GetHTTPTransport(insecure),
Timeout: 30 * time.Second,
}
if len(scopeType) != 0 || len(scopeName) != 0 {
authorizer.scope = &Scope{
Type: scopeType,
Name: scopeName,
Actions: scopeActions,
generator := &standardTokenGenerator{
credential: credential,
client: client,
}
// when the registry client is used inside Harbor, the token request
// can be posted to token service directly rather than going through nginx.
// If realm is set as the internal url of token service, this can resolve
// two problems:
// 1. performance issue
// 2. the realm field returned by registry is an IP which can not reachable
// inside Harbor
if len(customizedTokenService) > 0 {
generator.realm = customizedTokenService[0]
}
return &tokenAuthorizer{
cachedTokens: make(map[string]*models.Token),
generator: generator,
client: client,
}
}
// standardTokenGenerator implements interface tokenGenerator
type standardTokenGenerator struct {
realm string
service string
credential Credential
client *http.Client
}
// get token from token service
func (s *standardTokenGenerator) generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) {
// ping first if the realm or service is null
if len(s.realm) == 0 || len(s.service) == 0 {
realm, service, err := ping(s.client, endpoint)
if err != nil {
return nil, err
}
if len(realm) == 0 {
log.Warning("empty realm, skip")
return nil, nil
}
if len(s.realm) == 0 {
s.realm = realm
}
s.service = service
}
authorizer.tg = authorizer.generateToken
return authorizer
return getToken(s.client, s.credential, s.realm, s.service, scopes)
}
func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (string, int, *time.Time, error) {
realm = s.tokenURL(realm)
tk, err := getToken(s.client, s.credential, realm,
service, scopes)
if err != nil {
return "", 0, nil, err
}
if len(tk.IssuedAt) == 0 {
return tk.Token, tk.ExpiresIn, nil, nil
}
issuedAt, err := time.Parse(time.RFC3339, tk.IssuedAt)
if err != nil {
return "", 0, nil, err
}
return tk.Token, tk.ExpiresIn, &issuedAt, nil
}
// when the registry client is used inside Harbor, the token request
// can be posted to token service directly rather than going through nginx.
// If realm is set as the internal url of token service, this can resolve
// two problems:
// 1. performance issue
// 2. the realm field returned by registry is an IP which can not reachable
// inside Harbor
func (s *standardTokenAuthorizer) tokenURL(realm string) string {
if len(s.tokenServiceEndpoint) != 0 {
return s.tokenServiceEndpoint
}
return realm
}
// Implements interface Handler
type usernameTokenAuthorizer struct {
tokenAuthorizer
username string
}
// NewRegistryUsernameTokenAuthorizer returns an authorizer to generate token for registry according to
// the user's privileges
func NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName string, scopeActions ...string) Authorizer {
return newUsernameTokenAuthorizer(false, username, scopeType, scopeName, scopeActions...)
}
// NewNotaryUsernameTokenAuthorizer returns an authorizer to generate token for notary according to
// the user's privileges
func NewNotaryUsernameTokenAuthorizer(username, scopeType, scopeName string, scopeActions ...string) Authorizer {
return newUsernameTokenAuthorizer(true, username, scopeType, scopeName, scopeActions...)
}
// newUsernameTokenAuthorizer returns a authorizer which will generate a token according to
// the user's privileges
func newUsernameTokenAuthorizer(notary bool, username, scopeType, scopeName string, scopeActions ...string) Authorizer {
authorizer := &usernameTokenAuthorizer{
// NewRawTokenAuthorizer returns a token authorizer which calls method to create
// token directly
func NewRawTokenAuthorizer(username, service string) registry.Modifier {
generator := &rawTokenGenerator{
service: service,
username: username,
}
authorizer.scope = &Scope{
Type: scopeType,
Name: scopeName,
Actions: scopeActions,
return &tokenAuthorizer{
cachedTokens: make(map[string]*models.Token),
generator: generator,
}
if notary {
authorizer.tg = authorizer.genNotaryToken
} else {
authorizer.tg = authorizer.genRegistryToken
}
return authorizer
}
func (u *usernameTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
token, expiresIn, issuedAt, err = token_util.RegistryTokenForUI(u.username, service, scopes)
return
// rawTokenGenerator implements interface tokenGenerator
type rawTokenGenerator struct {
service string
username string
}
func (u *usernameTokenAuthorizer) genRegistryToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
token, expiresIn, issuedAt, err = token_util.RegistryTokenForUI(u.username, service, scopes)
return
// generate token directly
func (r *rawTokenGenerator) generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) {
return token_util.MakeToken(r.username, r.service, scopes)
}
func (u *usernameTokenAuthorizer) genNotaryToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
token, expiresIn, issuedAt, err = token_util.NotaryTokenForUI(u.username, service, scopes)
return
func buildPingURL(endpoint string) string {
return fmt.Sprintf("%s/v2/", endpoint)
}

View File

@ -15,54 +15,195 @@
package auth
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/docker/distribution/registry/auth/token"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/test"
)
func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) {
handler := test.Handler(&test.Response{
Body: []byte(`
{
"token":"token",
"expires_in":300,
"issued_at":"2016-08-17T23:17:58+08:00"
}
`),
func TestFilterReq(t *testing.T) {
authorizer := tokenAuthorizer{}
// v2
req, err := http.NewRequest(http.MethodGet, "http://registry/v2/", nil)
require.Nil(t, err)
goon, err := authorizer.filterReq(req)
assert.Nil(t, err)
assert.True(t, goon)
// catalog
req, err = http.NewRequest(http.MethodGet, "http://registry/v2/_catalog?n=1000", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.True(t, goon)
// contains two v2 in path
req, err = http.NewRequest(http.MethodGet, "http://registry/v2/library/v2/tags/list", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.True(t, goon)
// different scheme
req, err = http.NewRequest(http.MethodGet, "https://registry/v2/library/golang/tags/list", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.False(t, goon)
// different host
req, err = http.NewRequest(http.MethodGet, "http://vmware.com/v2/library/golang/tags/list", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.False(t, goon)
// different path
req, err = http.NewRequest(http.MethodGet, "https://registry/s3/ssss", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.False(t, goon)
}
func TestParseScopes(t *testing.T) {
// contains from in query string
req, err := http.NewRequest(http.MethodGet, "http://registry/v2?from=library", nil)
require.Nil(t, err)
scopses, err := parseScopes(req)
assert.Nil(t, err)
assert.Equal(t, 1, len(scopses))
assert.EqualValues(t, &token.ResourceActions{
Type: "repository",
Name: "library",
Actions: []string{
"pull"},
}, scopses[0])
// v2
req, err = http.NewRequest(http.MethodGet, "http://registry/v2", nil)
require.Nil(t, err)
scopses, err = parseScopes(req)
assert.Nil(t, err)
assert.Equal(t, 0, len(scopses))
// catalog
req, err = http.NewRequest(http.MethodGet, "http://registry/v2/_catalog", nil)
require.Nil(t, err)
scopses, err = parseScopes(req)
assert.Nil(t, err)
assert.Equal(t, 1, len(scopses))
assert.EqualValues(t, &token.ResourceActions{
Type: "registry",
Name: "catalog",
Actions: []string{
"*"},
}, scopses[0])
// manifest
req, err = http.NewRequest(http.MethodPut, "http://registry/v2/library/mysql/5.6/manifests/1", nil)
require.Nil(t, err)
scopses, err = parseScopes(req)
assert.Nil(t, err)
assert.Equal(t, 1, len(scopses))
assert.EqualValues(t, &token.ResourceActions{
Type: "repository",
Name: "library/mysql/5.6",
Actions: []string{
"push"},
}, scopses[0])
// invalid
req, err = http.NewRequest(http.MethodPut, "http://registry/other", nil)
require.Nil(t, err)
scopses, err = parseScopes(req)
assert.NotNil(t, err)
}
func TestGetAndUpdateCachedToken(t *testing.T) {
authorizer := &tokenAuthorizer{
cachedTokens: make(map[string]*models.Token),
}
// empty cache
token := authorizer.getCachedToken("")
assert.Nil(t, token)
// put a valid token into cache
token = &models.Token{
Token: "token",
ExpiresIn: 60,
IssuedAt: time.Now().Format(time.RFC3339),
}
authorizer.updateCachedToken("", token)
token2 := authorizer.getCachedToken("")
assert.EqualValues(t, token, token2)
// put a expired token into cache
token = &models.Token{
Token: "token",
ExpiresIn: 60,
IssuedAt: time.Now().Add(-time.Second * 120).Format("2006-01-02 15:04:05.999999999 -0700 MST"),
}
authorizer.updateCachedToken("", token)
token2 = authorizer.getCachedToken("")
assert.Nil(t, token2)
}
func TestModifyOfStandardTokenAuthorizer(t *testing.T) {
token := &models.Token{
Token: "token",
ExpiresIn: 3600,
IssuedAt: time.Now().String(),
}
data, err := json.Marshal(token)
require.Nil(t, err)
tokenHandler := test.Handler(&test.Response{
Body: data,
})
server := test.NewServer(&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/token",
Handler: handler,
tokenServer := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/service/token",
Handler: tokenHandler,
})
defer tokenServer.Close()
header := fmt.Sprintf("Bearer realm=\"%s/service/token\",service=\"registry\"",
tokenServer.URL)
pingHandler := test.Handler(&test.Response{
StatusCode: http.StatusUnauthorized,
Headers: map[string]string{
"WWW-Authenticate": header,
},
})
defer server.Close()
registryServer := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/v2",
Handler: pingHandler,
})
defer registryServer.Close()
authorizer := NewStandardTokenAuthorizer(nil, false, "", "repository", "library/ubuntu", "pull")
req, err := http.NewRequest("GET", "http://registry", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/", registryServer.URL), nil)
require.Nil(t, err)
params := map[string]string{
"realm": server.URL + "/token",
}
authorizer := NewStandardTokenAuthorizer(nil, false)
if err := authorizer.Authorize(req, params); err != nil {
t.Fatalf("failed to authorize request: %v", err)
}
err = authorizer.Modify(req)
require.Nil(t, err)
tk := req.Header.Get("Authorization")
if tk != "Bearer token" {
t.Errorf("unexpected token: %s != %s", tk, "Bearer token")
}
}
func TestSchemeOfStandardTokenAuthorizer(t *testing.T) {
authorizer := &standardTokenAuthorizer{}
if authorizer.Scheme() != "bearer" {
t.Errorf("unexpected scheme: %s != %s", authorizer.Scheme(), "bearer")
}
assert.Equal(t, strings.ToLower("Bearer "+token.Token), strings.ToLower(tk))
}

View File

@ -20,8 +20,8 @@ import (
"net/http"
"net/url"
"github.com/docker/distribution/registry/auth/token"
"github.com/vmware/harbor/src/common/models"
registry_error "github.com/vmware/harbor/src/common/utils/error"
"github.com/vmware/harbor/src/common/utils/registry"
)
@ -32,21 +32,16 @@ const (
// GetToken requests a token against the endpoint using credetial provided
func GetToken(endpoint string, insecure bool, credential Credential,
scopes []*Scope) (*models.Token, error) {
scopes []*token.ResourceActions) (*models.Token, error) {
client := &http.Client{
Transport: registry.GetHTTPTransport(insecure),
}
scopesStr := []string{}
for _, scope := range scopes {
scopesStr = append(scopesStr, scope.string())
}
return getToken(client, credential, endpoint, service, scopesStr)
return getToken(client, credential, endpoint, service, scopes)
}
func getToken(client *http.Client, credential Credential, realm, service string,
scopes []string) (*models.Token, error) {
scopes []*token.ResourceActions) (*models.Token, error) {
u, err := url.Parse(realm)
if err != nil {
return nil, err
@ -54,7 +49,7 @@ func getToken(client *http.Client, credential Credential, realm, service string,
query := u.Query()
query.Add("service", service)
for _, scope := range scopes {
query.Add("scope", scope)
query.Add("scope", scopeString(scope))
}
u.RawQuery = query.Encode()

View File

@ -50,7 +50,7 @@ func (isj *ImageScanJob) Post() {
}
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
repoClient, err := utils.NewRepositoryClient(regURL, false, auth.NewCookieCredential(c),
config.InternalTokenServiceEndpoint(), data.Repo, "pull", "push", "*")
config.InternalTokenServiceEndpoint(), data.Repo)
if err != nil {
log.Errorf("An error occurred while creating repository client: %v", err)
isj.RenderError(http.StatusInternalServerError, "Failed to repository client")

View File

@ -132,7 +132,7 @@ func (i *Initializer) enter() (string, error) {
c := &http.Cookie{Name: models.UISecretCookie, Value: i.srcSecret}
srcCred := auth.NewCookieCredential(c)
srcClient, err := utils.NewRepositoryClient(i.srcURL, i.insecure, srcCred,
config.InternalTokenServiceEndpoint(), i.repository, "pull", "push", "*")
config.InternalTokenServiceEndpoint(), i.repository)
if err != nil {
i.logger.Errorf("an error occurred while creating source repository client: %v", err)
return "", err
@ -141,7 +141,7 @@ func (i *Initializer) enter() (string, error) {
dstCred := auth.NewBasicAuthCredential(i.dstUsr, i.dstPwd)
dstClient, err := utils.NewRepositoryClient(i.dstURL, i.insecure, dstCred,
"", i.repository, "pull", "push", "*")
"", i.repository)
if err != nil {
i.logger.Errorf("an error occurred while creating destination repository client: %v", err)
return "", err

View File

@ -43,7 +43,7 @@ func (iz *Initializer) Enter() (string, error) {
}
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
repoClient, err := utils.NewRepositoryClient(regURL, false, auth.NewCookieCredential(c),
config.InternalTokenServiceEndpoint(), iz.Context.Repository, "pull")
config.InternalTokenServiceEndpoint(), iz.Context.Repository)
if err != nil {
logger.Errorf("An error occurred while creating repository client: %v", err)
return "", err

View File

@ -18,6 +18,7 @@ import (
"fmt"
"net/http"
"github.com/docker/distribution/registry/auth/token"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
@ -26,24 +27,15 @@ import (
//NewRepositoryClient create a repository client with scope type "reopsitory" and scope as the repository it would access.
func NewRepositoryClient(endpoint string, insecure bool, credential auth.Credential,
tokenServiceEndpoint, repository string, actions ...string) (*registry.Repository, error) {
tokenServiceEndpoint, repository string) (*registry.Repository, error) {
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure,
tokenServiceEndpoint, "repository", repository, actions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
tokenServiceEndpoint)
uam := &userAgentModifier{
userAgent: "harbor-registry-client",
}
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store, uam)
if err != nil {
return nil, err
}
return client, nil
return registry.NewRepositoryWithModifiers(repository, endpoint, insecure, authorizer, uam)
}
type userAgentModifier struct {
@ -65,14 +57,15 @@ func BuildBlobURL(endpoint, repository, digest string) string {
func GetTokenForRepo(repository string) (string, error) {
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
credentail := auth.NewCookieCredential(c)
token, err := auth.GetToken(config.InternalTokenServiceEndpoint(), true, credentail, []*auth.Scope{&auth.Scope{
Type: "repository",
Name: repository,
Actions: []string{"pull"},
}})
t, err := auth.GetToken(config.InternalTokenServiceEndpoint(), true, credentail,
[]*token.ResourceActions{&token.ResourceActions{
Type: "repository",
Name: repository,
Actions: []string{"pull"},
}})
if err != nil {
return "", err
}
return token.Token, nil
return t.Token, nil
}

View File

@ -190,7 +190,7 @@ func (ra *RepositoryAPI) Delete() {
return
}
rc, err := ra.initRepositoryClient(repoName)
rc, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -306,7 +306,7 @@ func (ra *RepositoryAPI) GetTag() {
return
}
client, err := ra.initRepositoryClient(repository)
client, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repository)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to initialize the client for %s: %v",
repository, err))
@ -355,7 +355,7 @@ func (ra *RepositoryAPI) GetTags() {
return
}
client, err := ra.initRepositoryClient(repoName)
client, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -496,7 +496,7 @@ func (ra *RepositoryAPI) GetManifests() {
return
}
rc, err := ra.initRepositoryClient(repoName)
rc, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -558,16 +558,6 @@ func getManifest(client *registry.Repository,
return result, nil
}
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint, err := config.RegistryURL()
if err != nil {
return nil, err
}
return uiutils.NewRepositoryClientForUI(endpoint, true, ra.SecurityCtx.GetUsername(),
repoName, "pull", "push", "*")
}
//GetTopRepos returns the most populor repositories
func (ra *RepositoryAPI) GetTopRepos() {
count, err := ra.GetInt("count", 10)
@ -808,7 +798,7 @@ func (ra *RepositoryAPI) checkExistence(repository, tag string) (bool, string, e
log.Errorf("project %s not found", project)
return false, "", nil
}
client, err := ra.initRepositoryClient(repository)
client, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repository)
if err != nil {
return false, "", fmt.Errorf("failed to initialize the client for %s: %v", repository, err)
}

View File

@ -25,7 +25,6 @@ import (
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
uiutils "github.com/vmware/harbor/src/ui/utils"
)
@ -165,13 +164,7 @@ func filterRepositories(projects []*models.Project, keyword string) (
}
func getTags(repository string) ([]string, error) {
url, err := config.RegistryURL()
if err != nil {
return nil, err
}
client, err := uiutils.NewRepositoryClientForUI(url, true,
"admin", repository, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repository)
if err != nil {
return nil, err
}

View File

@ -24,10 +24,10 @@ import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
registry_error "github.com/vmware/harbor/src/common/utils/error"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
registry_error "github.com/vmware/harbor/src/common/utils/error"
"github.com/vmware/harbor/src/ui/config"
)
@ -64,8 +64,7 @@ func (t *TargetAPI) ping(endpoint, username, password string) {
log.Errorf("failed to check whether insecure or not: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
registry, err := newRegistryClient(endpoint, !verify, username, password,
"", "", "")
registry, err := newRegistryClient(endpoint, !verify, username, password)
if err != nil {
// timeout, dns resolve error, connection refused, etc.
if urlErr, ok := err.(*url.Error); ok {
@ -345,23 +344,10 @@ func (t *TargetAPI) Delete() {
}
}
func newRegistryClient(endpoint string, insecure bool, username, password, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) {
func newRegistryClient(endpoint string, insecure bool, username, password string) (*registry.Registry, error) {
credential := auth.NewBasicAuthCredential(username, password)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure,
"", scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure)
return registry.NewRegistryWithModifiers(endpoint, insecure, authorizer)
}
// ListPolicies ...

View File

@ -34,6 +34,7 @@ import (
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/projectmanager"
"github.com/vmware/harbor/src/ui/service/token"
uiutils "github.com/vmware/harbor/src/ui/utils"
)
@ -279,12 +280,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
}
// TODO remove the workaround when the bug of registry is fixed
endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := uiutils.NewRepositoryClientForUI(endpoint, true,
"admin", repoInR, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repoInR)
if err != nil {
return needsAdd, needsDel, err
}
@ -304,11 +300,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
j++
} else {
// TODO remove the workaround when the bug of registry is fixed
endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := uiutils.NewRepositoryClientForUI(endpoint, true, "admin", repoInR, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repoInR)
if err != nil {
return needsAdd, needsDel, err
}
@ -340,12 +332,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
continue
}
endpoint, err := config.RegistryURL()
if err != nil {
log.Errorf("failed to get registry URL: %v", err)
continue
}
client, err := uiutils.NewRepositoryClientForUI(endpoint, true, "admin", repoInR, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repoInR)
if err != nil {
log.Errorf("failed to create repository client: %v", err)
continue
@ -377,7 +364,6 @@ func projectExists(pm projectmanager.ProjectManager, repository string) (bool, e
return pm.Exist(project)
}
// TODO need a registry client which accept a raw token as param
func initRegistryClient() (r *registry.Registry, err error) {
endpoint, err := config.RegistryURL()
if err != nil {
@ -393,12 +379,8 @@ func initRegistryClient() (r *registry.Registry, err error) {
return nil, err
}
registryClient, err := NewRegistryClient(endpoint, true, "admin",
"registry", "catalog", "*")
if err != nil {
return nil, err
}
return registryClient, nil
authorizer := auth.NewRawTokenAuthorizer("harbor-ui", token.Registry)
return registry.NewRegistryWithModifiers(endpoint, true, authorizer)
}
func buildReplicationURL() string {
@ -427,24 +409,6 @@ func repositoryExist(name string, client *registry.Repository) (bool, error) {
return len(tags) != 0, nil
}
// NewRegistryClient ...
// TODO need a registry client which accept a raw token as param
func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) {
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
}
// transformVulnerabilities transforms the returned value of Clair API to a list of VulnerabilityItem
func transformVulnerabilities(layerWithVuln *models.ClairLayerEnvelope) []*models.VulnerabilityItem {
res := []*models.VulnerabilityItem{}

View File

@ -191,6 +191,7 @@ func TokenExpiration() (int, error) {
if err != nil {
return 0, err
}
return int(cfg[common.TokenExpiration].(float64)), nil
}

View File

@ -30,9 +30,10 @@ func TestMain(m *testing.M) {
defer notaryServer.Close()
NotaryEndpoint = notaryServer.URL
var defaultConfig = map[string]interface{}{
common.ExtEndpoint: "https://" + endpoint,
common.WithNotary: true,
common.CfgExpiration: 5,
common.ExtEndpoint: "https://" + endpoint,
common.WithNotary: true,
common.CfgExpiration: 5,
common.TokenExpiration: 30,
}
adminServer, err := utilstest.NewAdminserver(defaultConfig)
if err != nil {
@ -117,6 +118,7 @@ func TestPMSPolicyChecker(t *testing.T) {
common.WithNotary: true,
common.CfgExpiration: 5,
common.AdmiralEndpoint: admiralEndpoint,
common.TokenExpiration: 30,
}
adminServer, err := utilstest.NewAdminserver(defaultConfigAdmiral)
if err != nil {

View File

@ -27,7 +27,7 @@ const (
manifestURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})`
imageInfoCtxKey = contextKey("ImageInfo")
//TODO: temp solution, remove after vmware/harbor#2242 is resolved.
tokenUsername = "admin"
tokenUsername = "harbor-ui"
)
// Record the docker deamon raw response.

View File

@ -93,54 +93,26 @@ func filterAccess(access []*token.ResourceActions, ctx security.Context,
return nil
}
// TODO merge RegistryTokenForUI NotaryTokenForUI genTokenForUI
// to one function
//RegistryTokenForUI calls genTokenForUI to get raw token for registry
func RegistryTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) {
return genTokenForUI(username, service, scopes)
}
//NotaryTokenForUI calls genTokenForUI to get raw token for notary
func NotaryTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) {
return genTokenForUI(username, service, scopes)
}
// genTokenForUI is for the UI process to call, so it won't establish a https connection from UI to proxy.
func genTokenForUI(username string, service string,
scopes []string) (string, int, *time.Time, error) {
access := GetResourceActions(scopes)
return MakeRawToken(username, service, access)
}
// MakeRawToken makes a valid jwt token based on parms.
func MakeRawToken(username, service string, access []*token.ResourceActions) (token string, expiresIn int, issuedAt *time.Time, err error) {
// MakeToken makes a valid jwt token based on parms.
func MakeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) {
pk, err := libtrust.LoadKeyFile(privateKey)
if err != nil {
return "", 0, nil, err
return nil, err
}
expiration, err := config.TokenExpiration()
if err != nil {
return "", 0, nil, err
return nil, err
}
tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk)
if err != nil {
return "", 0, nil, err
}
rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature))
return rs, expiresIn, issuedAt, nil
}
func makeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) {
raw, expires, issued, err := MakeRawToken(username, service, access)
if err != nil {
return nil, err
}
rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature))
return &models.Token{
Token: raw,
ExpiresIn: expires,
IssuedAt: issued.Format(time.RFC3339),
Token: rs,
ExpiresIn: expiresIn,
IssuedAt: issuedAt.Format(time.RFC3339),
}, nil
}

View File

@ -33,8 +33,10 @@ var registryFilterMap map[string]accessFilter
var notaryFilterMap map[string]accessFilter
const (
notary = "harbor-notary"
registry = "harbor-registry"
// Notary service
Notary = "harbor-notary"
// Registry service
Registry = "harbor-registry"
)
//InitCreators initialize the token creators for different services
@ -57,14 +59,14 @@ func InitCreators() {
},
},
}
creatorMap[notary] = &generalCreator{
service: notary,
creatorMap[Notary] = &generalCreator{
service: Notary,
filterMap: notaryFilterMap,
}
}
creatorMap[registry] = &generalCreator{
service: registry,
creatorMap[Registry] = &generalCreator{
service: Registry,
filterMap: registryFilterMap,
}
}
@ -200,7 +202,7 @@ func (g generalCreator) Create(r *http.Request) (*models.Token, error) {
if err != nil {
return nil, err
}
return makeToken(ctx.GetUsername(), g.service, access)
return MakeToken(ctx.GetUsername(), g.service, access)
}
func parseScopes(u *url.URL) []string {

View File

@ -111,7 +111,7 @@ func TestMakeToken(t *testing.T) {
}}
svc := "harbor-registry"
u := "tester"
tokenJSON, err := makeToken(u, svc, ra)
tokenJSON, err := MakeToken(u, svc, ra)
if err != nil {
t.Errorf("Error while making token: %v", err)
}

View File

@ -22,6 +22,7 @@ import (
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/service/token"
"bytes"
"encoding/json"
@ -32,11 +33,6 @@ import (
// ScanAllImages scans all images of Harbor by submiting jobs to jobservice, the whole process will move on if failed to submit any job of a single image.
func ScanAllImages() error {
regURL, err := config.RegistryURL()
if err != nil {
log.Errorf("Failed to load registry url")
return err
}
repos, err := dao.GetAllRepositories()
if err != nil {
log.Errorf("Failed to list all repositories, error: %v", err)
@ -44,33 +40,28 @@ func ScanAllImages() error {
}
log.Infof("Scanning all images on Harbor.")
go scanRepos(repos, regURL)
go scanRepos(repos)
return nil
}
// ScanImagesByProjectID scans all images under a projet, the whole process will move on if failed to submit any job of a single image.
func ScanImagesByProjectID(id int64) error {
regURL, err := config.RegistryURL()
if err != nil {
log.Errorf("Failed to load registry url")
return err
}
repos, err := dao.GetRepositoriesByProject(id, "", 0, 0)
if err != nil {
log.Errorf("Failed list repositories in project %d, error: %v", id, err)
return err
}
log.Infof("Scanning all images in project: %d ", id)
go scanRepos(repos, regURL)
go scanRepos(repos)
return nil
}
func scanRepos(repos []*models.RepoRecord, regURL string) {
func scanRepos(repos []*models.RepoRecord) {
var repoClient *registry.Repository
var err error
var tags []string
for _, r := range repos {
repoClient, err = NewRepositoryClientForUI(regURL, true, "harbor-ui", r.Name, "pull")
repoClient, err = NewRepositoryClientForUI("harbor-ui", r.Name)
if err != nil {
log.Errorf("Failed to initialize client for repository: %s, error: %v, skip scanning", r.Name, err)
continue
@ -131,20 +122,15 @@ func TriggerImageScan(repository string, tag string) error {
return RequestAsUI("POST", url, bytes.NewBuffer(b), NewStatusRespHandler(http.StatusOK))
}
// NewRepositoryClientForUI ...
// TODO need a registry client which accept a raw token as param
func NewRepositoryClientForUI(endpoint string, insecure bool, username, repository string,
scopeActions ...string) (*registry.Repository, error) {
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, "repository", repository, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
// NewRepositoryClientForUI creates a repository client that can only be used to
// access the internal registry
func NewRepositoryClientForUI(username, repository string) (*registry.Repository, error) {
endpoint, err := config.RegistryURL()
if err != nil {
return nil, err
}
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
insecure := true
authorizer := auth.NewRawTokenAuthorizer(username, token.Registry)
return registry.NewRepositoryWithModifiers(repository, endpoint, insecure, authorizer)
}