mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-12 11:56:56 +01:00
Merge pull request #1462 from reasonerjt/token-service-refactory
Token service refactory
This commit is contained in:
commit
1405661f5f
@ -20,8 +20,8 @@
|
|||||||
"type": "token",
|
"type": "token",
|
||||||
"options": {
|
"options": {
|
||||||
"realm": "$token_endpoint/service/token",
|
"realm": "$token_endpoint/service/token",
|
||||||
"service": "token-service",
|
"service": "harbor-registry",
|
||||||
"issuer": "registry-token-issuer",
|
"issuer": "harbor-token-issuer",
|
||||||
"rootcertbundle": "/config/root.crt"
|
"rootcertbundle": "/config/root.crt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,10 @@ http:
|
|||||||
addr: localhost:5001
|
addr: localhost:5001
|
||||||
auth:
|
auth:
|
||||||
token:
|
token:
|
||||||
issuer: registry-token-issuer
|
issuer: harbor-token-issuer
|
||||||
realm: $ui_url/service/token
|
realm: $ui_url/service/token
|
||||||
rootcertbundle: /etc/registry/root.crt
|
rootcertbundle: /etc/registry/root.crt
|
||||||
service: token-service
|
service: harbor-registry
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
endpoints:
|
endpoints:
|
||||||
|
@ -24,7 +24,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var adminServerDefaultConfig = map[string]interface{}{
|
var adminServerDefaultConfig = map[string]interface{}{
|
||||||
config.ExtEndpoint: "host01.com",
|
config.ExtEndpoint: "https://host01.com",
|
||||||
config.AUTHMode: config.DBAuth,
|
config.AUTHMode: config.DBAuth,
|
||||||
config.DatabaseType: "mysql",
|
config.DatabaseType: "mysql",
|
||||||
config.MySQLHost: "127.0.0.1",
|
config.MySQLHost: "127.0.0.1",
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
_ "github.com/vmware/harbor/src/ui/auth/db"
|
_ "github.com/vmware/harbor/src/ui/auth/db"
|
||||||
_ "github.com/vmware/harbor/src/ui/auth/ldap"
|
_ "github.com/vmware/harbor/src/ui/auth/ldap"
|
||||||
"github.com/vmware/harbor/src/ui/config"
|
"github.com/vmware/harbor/src/ui/config"
|
||||||
|
"github.com/vmware/harbor/src/ui/service/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -78,7 +79,7 @@ func main() {
|
|||||||
log.Fatalf("failed to initialize configurations: %v", err)
|
log.Fatalf("failed to initialize configurations: %v", err)
|
||||||
}
|
}
|
||||||
log.Info("configurations initialization completed")
|
log.Info("configurations initialization completed")
|
||||||
|
token.InitCreators()
|
||||||
database, err := config.Database()
|
database, err := config.Database()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to get database configuration: %v", err)
|
log.Fatalf("failed to get database configuration: %v", err)
|
||||||
|
@ -33,7 +33,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
issuer = "registry-token-issuer"
|
issuer = "harbor-token-issuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var privateKey string
|
var privateKey string
|
||||||
@ -74,84 +74,31 @@ func GetResourceActions(scopes []string) []*token.ResourceActions {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterAccess modify the action list in access based on permission
|
|
||||||
func FilterAccess(username string, a *token.ResourceActions) {
|
|
||||||
|
|
||||||
if a.Type == "registry" && a.Name == "catalog" {
|
|
||||||
log.Infof("current access, type: %s, name:%s, actions:%v \n", a.Type, a.Name, a.Actions)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//clear action list to assign to new acess element after perm check.
|
|
||||||
a.Actions = []string{}
|
|
||||||
if a.Type == "repository" {
|
|
||||||
repoSplit := strings.Split(a.Name, "/")
|
|
||||||
repoLength := len(repoSplit)
|
|
||||||
if repoLength > 1 { //Only check the permission when the requested image has a namespace, i.e. project
|
|
||||||
var projectName string
|
|
||||||
registryURL, err := config.ExtEndpoint()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get domain name: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
registryURL = strings.Split(registryURL, "://")[1]
|
|
||||||
if repoSplit[0] == registryURL {
|
|
||||||
projectName = repoSplit[1]
|
|
||||||
log.Infof("Detected Registry URL in Project Name. Assuming this is a notary request and setting Project Name as %s\n", projectName)
|
|
||||||
} else {
|
|
||||||
projectName = repoSplit[0]
|
|
||||||
}
|
|
||||||
var permission string
|
|
||||||
if len(username) > 0 {
|
|
||||||
isAdmin, err := dao.IsAdminRole(username)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error occurred in IsAdminRole: %v", err)
|
|
||||||
}
|
|
||||||
if isAdmin {
|
|
||||||
exist, err := dao.ProjectExists(projectName)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error occurred in CheckExistProject: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if exist {
|
|
||||||
permission = "RWM"
|
|
||||||
} else {
|
|
||||||
permission = ""
|
|
||||||
log.Infof("project %s does not exist, set empty permission for admin\n", projectName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
permission, err = dao.GetPermission(username, projectName)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error occurred in GetPermission: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(permission, "W") {
|
|
||||||
a.Actions = append(a.Actions, "push")
|
|
||||||
}
|
|
||||||
if strings.Contains(permission, "M") {
|
|
||||||
a.Actions = append(a.Actions, "*")
|
|
||||||
}
|
|
||||||
if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) {
|
|
||||||
a.Actions = append(a.Actions, "pull")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Infof("current access, type: %s, name:%s, actions:%v \n", a.Type, a.Name, a.Actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenTokenForUI is for the UI process to call, so it won't establish a https connection from UI to proxy.
|
// 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) (token string, expiresIn int, issuedAt *time.Time, err error) {
|
func GenTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) {
|
||||||
|
isAdmin, err := dao.IsAdminRole(username)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, nil, err
|
||||||
|
}
|
||||||
|
f := &repositoryFilter{
|
||||||
|
parser: &basicParser{},
|
||||||
|
}
|
||||||
|
u := userInfo{
|
||||||
|
name: username,
|
||||||
|
allPerm: isAdmin,
|
||||||
|
}
|
||||||
access := GetResourceActions(scopes)
|
access := GetResourceActions(scopes)
|
||||||
for _, a := range access {
|
for _, a := range access {
|
||||||
FilterAccess(username, a)
|
err = f.filter(u, a)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return MakeToken(username, service, access)
|
return MakeRawToken(username, service, access)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeToken makes a valid jwt token based on parms.
|
// MakeRawToken makes a valid jwt token based on parms.
|
||||||
func MakeToken(username, service string, access []*token.ResourceActions) (token string, expiresIn int, issuedAt *time.Time, err error) {
|
func MakeRawToken(username, service string, access []*token.ResourceActions) (token string, expiresIn int, issuedAt *time.Time, err error) {
|
||||||
pk, err := libtrust.LoadKeyFile(privateKey)
|
pk, err := libtrust.LoadKeyFile(privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, nil, err
|
return "", 0, nil, err
|
||||||
@ -169,6 +116,34 @@ func MakeToken(username, service string, access []*token.ResourceActions) (token
|
|||||||
return rs, expiresIn, issuedAt, nil
|
return rs, expiresIn, issuedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tokenJSON struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
IssuedAt string `json:"issued_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeToken(username, service string, access []*token.ResourceActions) (*tokenJSON, error) {
|
||||||
|
raw, expires, issued, err := MakeRawToken(username, service, access)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tokenJSON{raw, expires, issued.Format(time.RFC3339)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func permToActions(p string) []string {
|
||||||
|
res := []string{}
|
||||||
|
if strings.Contains(p, "W") {
|
||||||
|
res = append(res, "push")
|
||||||
|
}
|
||||||
|
if strings.Contains(p, "M") {
|
||||||
|
res = append(res, "*")
|
||||||
|
}
|
||||||
|
if strings.Contains(p, "R") {
|
||||||
|
res = append(res, "pull")
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
//make token core
|
//make token core
|
||||||
func makeTokenCore(issuer, subject, audience string, expiration int,
|
func makeTokenCore(issuer, subject, audience string, expiration int,
|
||||||
access []*token.ResourceActions, signingKey libtrust.PrivateKey) (t *token.Token, expiresIn int, issuedAt *time.Time, err error) {
|
access []*token.ResourceActions, signingKey libtrust.PrivateKey) (t *token.Token, expiresIn int, issuedAt *time.Time, err error) {
|
||||||
|
239
src/ui/service/token/creator.go
Normal file
239
src/ui/service/token/creator.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2016 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 token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/distribution/registry/auth/token"
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/ui/config"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var creatorMap map[string]Creator
|
||||||
|
|
||||||
|
const (
|
||||||
|
notary = "harbor-notary"
|
||||||
|
registry = "harbor-registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
//InitCreators initialize the token creators for different services
|
||||||
|
func InitCreators() {
|
||||||
|
creatorMap = make(map[string]Creator)
|
||||||
|
ext, err := config.ExtEndpoint()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Failed to get ext enpoint, err: %v, the token service will not be functional with notary requests", err)
|
||||||
|
} else {
|
||||||
|
creatorMap[notary] = &generalCreator{
|
||||||
|
validators: []ReqValidator{
|
||||||
|
&basicAuthValidator{},
|
||||||
|
},
|
||||||
|
service: notary,
|
||||||
|
filterMap: map[string]accessFilter{
|
||||||
|
"repository": &repositoryFilter{
|
||||||
|
parser: &endpointParser{
|
||||||
|
endpoint: strings.Split(ext, "//")[1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
creatorMap[registry] = &generalCreator{
|
||||||
|
validators: []ReqValidator{
|
||||||
|
&secretValidator{config.JobserviceSecret()},
|
||||||
|
&basicAuthValidator{},
|
||||||
|
},
|
||||||
|
service: registry,
|
||||||
|
filterMap: map[string]accessFilter{
|
||||||
|
"repository": &repositoryFilter{
|
||||||
|
//Workaround, had to use same service for both notary and registry
|
||||||
|
parser: &endpointParser{
|
||||||
|
endpoint: ext,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"registry": ®istryFilter{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creator creates a token ready to be served based on the http request.
|
||||||
|
type Creator interface {
|
||||||
|
Create(r *http.Request) (*tokenJSON, 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)
|
||||||
|
}
|
||||||
|
//Workaround, need to use endpoint Parser to handle both cases.
|
||||||
|
if strings.ContainsRune(repo[0], '.') {
|
||||||
|
if repo[0] != e.endpoint {
|
||||||
|
log.Warningf("Mismatch endpoint from string: %s, expected endpoint: %s, fallback to basic parser", s, e.endpoint)
|
||||||
|
return parseImg(s)
|
||||||
|
}
|
||||||
|
return parseImg(repo[1])
|
||||||
|
}
|
||||||
|
return parseImg(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
//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(user userInfo, a *token.ResourceActions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type registryFilter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reg registryFilter) filter(user userInfo, 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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//repositoryFilter filters the access based on Harbor's permission model
|
||||||
|
type repositoryFilter struct {
|
||||||
|
parser imageParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rep repositoryFilter) filter(user userInfo, a *token.ResourceActions) error {
|
||||||
|
//clear action list to assign to new acess element after perm check.
|
||||||
|
a.Actions = []string{}
|
||||||
|
img, err := rep.parser.parse(a.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project := img.namespace
|
||||||
|
permission := ""
|
||||||
|
if user.allPerm {
|
||||||
|
exist, err := dao.ProjectExists(project)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error occurred in CheckExistProject: %v", err)
|
||||||
|
//just leave empty permission
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if exist {
|
||||||
|
permission = "RWM"
|
||||||
|
} else {
|
||||||
|
log.Infof("project %s does not exist, set empty permission for admin\n", project)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
permission, err = dao.GetPermission(user.name, project)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error occurred in GetPermission: %v", err)
|
||||||
|
//just leave empty permission
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if dao.IsProjectPublic(project) {
|
||||||
|
permission += "R"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.Actions = permToActions(permission)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type generalCreator struct {
|
||||||
|
validators []ReqValidator
|
||||||
|
service string
|
||||||
|
filterMap map[string]accessFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
type unauthorizedError struct{}
|
||||||
|
|
||||||
|
func (e *unauthorizedError) Error() string {
|
||||||
|
return "Unauthorized"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g generalCreator) Create(r *http.Request) (*tokenJSON, error) {
|
||||||
|
var user *userInfo
|
||||||
|
var err error
|
||||||
|
var scopes []string
|
||||||
|
scopeParm := r.URL.Query()["scope"]
|
||||||
|
if len(scopeParm) > 0 {
|
||||||
|
scopes = strings.Split(r.URL.Query()["scope"][0], " ")
|
||||||
|
}
|
||||||
|
log.Debugf("scopes: %v", scopes)
|
||||||
|
for _, v := range g.validators {
|
||||||
|
user, err = v.validate(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if user != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
if len(scopes) == 0 {
|
||||||
|
return nil, &unauthorizedError{}
|
||||||
|
}
|
||||||
|
user = &userInfo{}
|
||||||
|
}
|
||||||
|
access := GetResourceActions(scopes)
|
||||||
|
for _, a := range access {
|
||||||
|
f, ok := g.filterMap[a.Type]
|
||||||
|
if !ok {
|
||||||
|
log.Warningf("No filter found for access type: %s, skip.", a.Type)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = f.filter(*user, a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return makeToken(user.name, g.service, access)
|
||||||
|
}
|
@ -16,17 +16,11 @@
|
|||||||
package token
|
package token
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/vmware/harbor/src/common/models"
|
|
||||||
"github.com/vmware/harbor/src/common/utils/log"
|
|
||||||
"github.com/vmware/harbor/src/ui/auth"
|
|
||||||
"github.com/vmware/harbor/src/ui/config"
|
|
||||||
svc_utils "github.com/vmware/harbor/src/ui/service/utils"
|
|
||||||
|
|
||||||
"github.com/astaxie/beego"
|
"github.com/astaxie/beego"
|
||||||
"github.com/docker/distribution/registry/auth/token"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler handles request on /service/token, which is the auth provider for registry.
|
// Handler handles request on /service/token, which is the auth provider for registry.
|
||||||
@ -38,62 +32,24 @@ type Handler struct {
|
|||||||
// and parse service and scope based on docker registry v2 standard,
|
// and parse service and scope based on docker registry v2 standard,
|
||||||
// checkes the permission agains local DB and generates jwt token.
|
// checkes the permission agains local DB and generates jwt token.
|
||||||
func (h *Handler) Get() {
|
func (h *Handler) Get() {
|
||||||
|
|
||||||
var uid, password, username string
|
|
||||||
request := h.Ctx.Request
|
request := h.Ctx.Request
|
||||||
|
log.Debugf("URL for token request: %s", request.URL.String())
|
||||||
service := h.GetString("service")
|
service := h.GetString("service")
|
||||||
scopes := h.GetStrings("scope")
|
tokenCreator, ok := creatorMap[service]
|
||||||
access := GetResourceActions(scopes)
|
if !ok {
|
||||||
log.Infof("request url: %v", request.URL.String())
|
errMsg := fmt.Sprintf("Unable to handle service: %s", service)
|
||||||
|
log.Errorf(errMsg)
|
||||||
if svc_utils.VerifySecret(request, config.JobserviceSecret()) {
|
h.CustomAbort(http.StatusBadRequest, errMsg)
|
||||||
log.Debugf("Will grant all access as this request is from job service with legal secret.")
|
|
||||||
username = "job-service-user"
|
|
||||||
} else {
|
|
||||||
uid, password, _ = request.BasicAuth()
|
|
||||||
log.Debugf("uid for logging: %s", uid)
|
|
||||||
user := authenticate(uid, password)
|
|
||||||
if user == nil {
|
|
||||||
log.Warningf("login request with invalid credentials in token service, uid: %s", uid)
|
|
||||||
if len(scopes) == 0 {
|
|
||||||
h.CustomAbort(http.StatusUnauthorized, "")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
username = user.Username
|
|
||||||
}
|
|
||||||
log.Debugf("username for filtering access: %s.", username)
|
|
||||||
for _, a := range access {
|
|
||||||
FilterAccess(username, a)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
h.serveToken(username, service, access)
|
token, err := tokenCreator.Create(request)
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveToken(username, service string, access []*token.ResourceActions) {
|
|
||||||
writer := h.Ctx.ResponseWriter
|
|
||||||
//create token
|
|
||||||
rawToken, expiresIn, issuedAt, err := MakeToken(username, service, access)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to make token, error: %v", err)
|
if _, ok := err.(*unauthorizedError); ok {
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
h.CustomAbort(http.StatusUnauthorized, "")
|
||||||
return
|
}
|
||||||
|
log.Errorf("Unexpected error when creating the token, error: %v", err)
|
||||||
|
h.CustomAbort(http.StatusInternalServerError, "")
|
||||||
}
|
}
|
||||||
tk := make(map[string]interface{})
|
h.Data["json"] = token
|
||||||
tk["token"] = rawToken
|
|
||||||
tk["expires_in"] = expiresIn
|
|
||||||
tk["issued_at"] = issuedAt.Format(time.RFC3339)
|
|
||||||
h.Data["json"] = tk
|
|
||||||
h.ServeJSON()
|
h.ServeJSON()
|
||||||
}
|
|
||||||
|
|
||||||
func authenticate(principal, password string) *models.User {
|
|
||||||
user, err := auth.Login(models.AuthModel{
|
|
||||||
Principal: principal,
|
|
||||||
Password: password,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error occurred in UserLogin: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
@ -9,21 +9,28 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/test"
|
||||||
|
"github.com/vmware/harbor/src/ui/config"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/vmware/harbor/src/common/utils/log"
|
|
||||||
"github.com/vmware/harbor/src/ui/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
if err := config.Init(); err != nil {
|
server, err := test.NewAdminserver(nil)
|
||||||
log.Fatalf("failed to initialize configurations: %v", err)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if err := os.Setenv("ADMIN_SERVER_URL", server.URL); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := config.Init(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
result := m.Run()
|
result := m.Run()
|
||||||
if result != 0 {
|
if result != 0 {
|
||||||
os.Exit(result)
|
os.Exit(result)
|
||||||
@ -87,10 +94,11 @@ func TestMakeToken(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
svc := "harbor-registry"
|
svc := "harbor-registry"
|
||||||
u := "tester"
|
u := "tester"
|
||||||
tokenString, _, _, err := MakeToken(u, svc, ra)
|
tokenJSON, err := makeToken(u, svc, ra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while making token: %v", err)
|
t.Errorf("Error while making token: %v", err)
|
||||||
}
|
}
|
||||||
|
tokenString := tokenJSON.Token
|
||||||
//t.Logf("privatekey: %s, crt: %s", tokenString, crt)
|
//t.Logf("privatekey: %s, crt: %s", tokenString, crt)
|
||||||
pubKey, err := getPublicKey(crt)
|
pubKey, err := getPublicKey(crt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -102,13 +110,78 @@ func TestMakeToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return pubKey, nil
|
return pubKey, nil
|
||||||
})
|
})
|
||||||
t.Logf("validity: %v", tok.Valid)
|
t.Logf("Token validity: %v", tok.Valid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while parsing the token: %v", err)
|
t.Errorf("Error while parsing the token: %v", err)
|
||||||
}
|
}
|
||||||
claims := tok.Claims.(*harborClaims)
|
claims := tok.Claims.(*harborClaims)
|
||||||
t.Logf("claims: %+v", *claims)
|
|
||||||
assert.Equal(t, *(claims.Access[0]), *(ra[0]), "Access mismatch")
|
assert.Equal(t, *(claims.Access[0]), *(ra[0]), "Access mismatch")
|
||||||
assert.Equal(t, claims.Audience, svc, "Audience mismatch")
|
assert.Equal(t, claims.Audience, svc, "Audience mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPermToActions(t *testing.T) {
|
||||||
|
perm1 := "RWM"
|
||||||
|
perm2 := "MRR"
|
||||||
|
perm3 := ""
|
||||||
|
expect1 := []string{"push", "*", "pull"}
|
||||||
|
expect2 := []string{"*", "pull"}
|
||||||
|
expect3 := []string{}
|
||||||
|
res1 := permToActions(perm1)
|
||||||
|
res2 := permToActions(perm2)
|
||||||
|
res3 := permToActions(perm3)
|
||||||
|
assert.Equal(t, res1, expect1, fmt.Sprintf("actions mismatch for permission: %s", perm1))
|
||||||
|
assert.Equal(t, res2, expect2, fmt.Sprintf("actions mismatch for permission: %s", perm2))
|
||||||
|
assert.Equal(t, res3, expect3, fmt.Sprintf("actions mismatch for permission: %s", perm3))
|
||||||
|
}
|
||||||
|
|
||||||
|
type parserTestRec struct {
|
||||||
|
input string
|
||||||
|
expect image
|
||||||
|
expectError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
InitCreators()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicParser(t *testing.T) {
|
||||||
|
testList := []parserTestRec{parserTestRec{"library/ubuntu:14.04", image{"library", "ubuntu", "14.04"}, false},
|
||||||
|
parserTestRec{"test/hello", image{"test", "hello", ""}, false},
|
||||||
|
parserTestRec{"myimage:14.04", image{}, true},
|
||||||
|
parserTestRec{"org/team/img", image{"org", "team/img", ""}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &basicParser{}
|
||||||
|
for _, rec := range testList {
|
||||||
|
r, err := p.parse(rec.input)
|
||||||
|
if rec.expectError {
|
||||||
|
assert.Error(t, err, "Expected error for input: %s", rec.input)
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err, "Expected no error for input: %s", rec.input)
|
||||||
|
assert.Equal(t, rec.expect, *r, "result mismatch for input: %s", rec.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointParser(t *testing.T) {
|
||||||
|
p := &endpointParser{
|
||||||
|
"10.117.4.142:5000",
|
||||||
|
}
|
||||||
|
testList := []parserTestRec{parserTestRec{"10.117.4.142:5000/library/ubuntu:14.04", image{"library", "ubuntu", "14.04"}, false},
|
||||||
|
parserTestRec{"myimage:14.04", image{}, true},
|
||||||
|
//Test the temp workaround
|
||||||
|
parserTestRec{"10.117.4.142:80/library/myimage:14.04", image{"10.117.4.142:80", "library/myimage", "14.04"}, false},
|
||||||
|
parserTestRec{"library/myimage:14.04", image{"library", "myimage", "14.04"}, false},
|
||||||
|
parserTestRec{"10.117.4.142:5000/myimage:14.04", image{}, true},
|
||||||
|
parserTestRec{"10.117.4.142:5000/org/team/img", image{"org", "team/img", ""}, false},
|
||||||
|
}
|
||||||
|
for _, rec := range testList {
|
||||||
|
r, err := p.parse(rec.input)
|
||||||
|
if rec.expectError {
|
||||||
|
assert.Error(t, err, "Expected error for input: %s", rec.input)
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err, "Expected no error for input: %s", rec.input)
|
||||||
|
assert.Equal(t, rec.expect, *r, "result mismatch for input: %s", rec.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
84
src/ui/service/token/validator.go
Normal file
84
src/ui/service/token/validator.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2016 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 token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/ui/auth"
|
||||||
|
svc_utils "github.com/vmware/harbor/src/ui/service/utils"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//For filtering permission by token creators.
|
||||||
|
type userInfo struct {
|
||||||
|
name string
|
||||||
|
allPerm bool
|
||||||
|
}
|
||||||
|
|
||||||
|
//ReqValidator validates request based on different rules and returns userInfo
|
||||||
|
type ReqValidator interface {
|
||||||
|
validate(req *http.Request) (*userInfo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type secretValidator struct {
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobServiceUserInfo userInfo
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
jobServiceUserInfo = userInfo{
|
||||||
|
name: "job-service-user",
|
||||||
|
allPerm: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv secretValidator) validate(r *http.Request) (*userInfo, error) {
|
||||||
|
if svc_utils.VerifySecret(r, sv.secret) {
|
||||||
|
return &jobServiceUserInfo, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type basicAuthValidator struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ba basicAuthValidator) validate(r *http.Request) (*userInfo, error) {
|
||||||
|
uid, password, _ := r.BasicAuth()
|
||||||
|
user, err := auth.Login(models.AuthModel{
|
||||||
|
Principal: uid,
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error occurred in UserLogin: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
log.Warningf("Invalid credentials for uid: %s", uid)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
isAdmin, err := dao.IsAdminRole(user.UserID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error occurred in IsAdminRole: %v", err)
|
||||||
|
}
|
||||||
|
info := &userInfo{
|
||||||
|
name: user.Username,
|
||||||
|
allPerm: isAdmin,
|
||||||
|
}
|
||||||
|
return info, nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user