mirror of https://github.com/goharbor/harbor.git
230 lines
5.8 KiB
Go
230 lines
5.8 KiB
Go
// Copyright Project Harbor Authors
|
|
//
|
|
// 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 token
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/docker/distribution/registry/auth/token"
|
|
|
|
"github.com/goharbor/harbor/src/common/models"
|
|
"github.com/goharbor/harbor/src/common/rbac"
|
|
rbac_project "github.com/goharbor/harbor/src/common/rbac/project"
|
|
"github.com/goharbor/harbor/src/common/security"
|
|
"github.com/goharbor/harbor/src/controller/project"
|
|
"github.com/goharbor/harbor/src/lib/errors"
|
|
"github.com/goharbor/harbor/src/lib/log"
|
|
)
|
|
|
|
var creatorMap map[string]Creator
|
|
var registryFilterMap map[string]accessFilter
|
|
var actionScopeMap = map[rbac.Action]string{
|
|
// Scopes checked by distribution, see: https://github.com/docker/distribution/blob/master/registry/handlers/app.go
|
|
rbac.ActionPull: "pull",
|
|
rbac.ActionPush: "push",
|
|
rbac.ActionDelete: "delete",
|
|
// For skipping policy check when scanner pulls artifacts
|
|
rbac.ActionScannerPull: "scanner-pull",
|
|
}
|
|
|
|
const (
|
|
// Registry service
|
|
Registry = "harbor-registry"
|
|
)
|
|
|
|
// InitCreators initialize the token creators for different services
|
|
func InitCreators() {
|
|
creatorMap = make(map[string]Creator)
|
|
registryFilterMap = map[string]accessFilter{
|
|
"repository": &repositoryFilter{
|
|
parser: &basicParser{},
|
|
},
|
|
"registry": ®istryFilter{},
|
|
}
|
|
creatorMap[Registry] = &generalCreator{
|
|
service: Registry,
|
|
filterMap: registryFilterMap,
|
|
}
|
|
}
|
|
|
|
// Creator creates a token ready to be served based on the http request.
|
|
type Creator interface {
|
|
Create(r *http.Request) (*models.Token, error)
|
|
}
|
|
|
|
type imageParser interface {
|
|
parse(s string) (*image, error)
|
|
}
|
|
|
|
type image struct {
|
|
namespace string
|
|
repo string
|
|
tag string
|
|
}
|
|
|
|
type basicParser struct{}
|
|
|
|
func (b basicParser) parse(s string) (*image, error) {
|
|
return parseImg(s)
|
|
}
|
|
|
|
type endpointParser struct {
|
|
endpoint string
|
|
}
|
|
|
|
func (e endpointParser) parse(s string) (*image, error) {
|
|
repo := strings.SplitN(s, "/", 2)
|
|
if len(repo) < 2 {
|
|
return nil, fmt.Errorf("unable to parse image from string: %s", s)
|
|
}
|
|
if repo[0] != e.endpoint {
|
|
return nil, fmt.Errorf("mismatch endpoint from string: %s, expected endpoint: %s", s, e.endpoint)
|
|
}
|
|
return parseImg(repo[1])
|
|
}
|
|
|
|
// build Image accepts a string like library/ubuntu:14.04 and build a image struct
|
|
func parseImg(s string) (*image, error) {
|
|
repo := strings.SplitN(s, "/", 2)
|
|
if len(repo) < 2 {
|
|
return nil, fmt.Errorf("unable to parse image from string: %s", s)
|
|
}
|
|
i := strings.SplitN(repo[1], ":", 2)
|
|
res := &image{
|
|
namespace: repo[0],
|
|
repo: i[0],
|
|
}
|
|
if len(i) == 2 {
|
|
res.tag = i[1]
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// An accessFilter will filter access based on userinfo
|
|
type accessFilter interface {
|
|
filter(ctx context.Context, ctl project.Controller, a *token.ResourceActions) error
|
|
}
|
|
|
|
type registryFilter struct {
|
|
}
|
|
|
|
func (reg registryFilter) filter(ctx context.Context, _ project.Controller,
|
|
a *token.ResourceActions) error {
|
|
// Do not filter if the request is to access registry catalog
|
|
if a.Name != "catalog" {
|
|
return fmt.Errorf("unable to handle, type: %s, name: %s", a.Type, a.Name)
|
|
}
|
|
|
|
secCtx, ok := security.FromContext(ctx)
|
|
if !ok || !secCtx.IsSysAdmin() {
|
|
// Set the actions to empty is the user is not admin
|
|
a.Actions = []string{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// repositoryFilter filters the access based on Harbor's permission model
|
|
type repositoryFilter struct {
|
|
parser imageParser
|
|
}
|
|
|
|
func (rep repositoryFilter) filter(ctx context.Context, ctl project.Controller,
|
|
a *token.ResourceActions) error {
|
|
// clear action list to assign to new acess element after perm check.
|
|
img, err := rep.parser.parse(a.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
projectName := img.namespace
|
|
|
|
project, err := ctl.GetByName(ctx, projectName)
|
|
if err != nil {
|
|
if errors.IsNotFoundErr(err) {
|
|
log.Debugf("project %s does not exist, set empty permission", projectName)
|
|
a.Actions = []string{}
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
resource := rbac_project.NewNamespace(project.ProjectID).Resource(rbac.ResourceRepository)
|
|
scopeList := make([]string, 0)
|
|
for s := range resourceScopes(ctx, resource) {
|
|
scopeList = append(scopeList, s)
|
|
}
|
|
a.Actions = scopeList
|
|
return nil
|
|
}
|
|
|
|
func resourceScopes(ctx context.Context, rc rbac.Resource) map[string]struct{} {
|
|
sCtx, _ := security.FromContext(ctx)
|
|
res := map[string]struct{}{}
|
|
for a, s := range actionScopeMap {
|
|
if sCtx.Can(ctx, a, rc) {
|
|
res[s] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
type generalCreator struct {
|
|
service string
|
|
filterMap map[string]accessFilter
|
|
}
|
|
|
|
type unauthorizedError struct{}
|
|
|
|
func (e *unauthorizedError) Error() string {
|
|
return "Unauthorized"
|
|
}
|
|
|
|
func (g generalCreator) Create(r *http.Request) (*models.Token, error) {
|
|
var err error
|
|
scopes := parseScopes(r.URL)
|
|
log.Debugf("scopes: %v", scopes)
|
|
|
|
ctx, ok := security.FromContext(r.Context())
|
|
if !ok {
|
|
return nil, fmt.Errorf("failed to get security context from request")
|
|
}
|
|
|
|
// for docker login
|
|
if !ctx.IsAuthenticated() {
|
|
if len(scopes) == 0 {
|
|
return nil, &unauthorizedError{}
|
|
}
|
|
}
|
|
access := GetResourceActions(scopes)
|
|
err = filterAccess(r.Context(), access, project.Ctl, g.filterMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return MakeToken(r.Context(), ctx.GetUsername(), g.service, access)
|
|
}
|
|
|
|
func parseScopes(u *url.URL) []string {
|
|
var sector string
|
|
var result []string
|
|
for _, sector = range u.Query()["scope"] {
|
|
result = append(result, strings.Split(sector, " ")...)
|
|
}
|
|
return result
|
|
}
|