mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-25 11:46:43 +01:00
190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
/*
|
|
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 utils
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/vmware/harbor/dao"
|
|
|
|
"github.com/docker/distribution/registry/auth/token"
|
|
"github.com/docker/libtrust"
|
|
)
|
|
|
|
const (
|
|
issuer = "registry-token-issuer"
|
|
privateKey = "/etc/ui/private_key.pem"
|
|
expiration = 5 //minute
|
|
)
|
|
|
|
func GetResourceActions(scope string) []*token.ResourceActions {
|
|
var res []*token.ResourceActions
|
|
if scope == "" {
|
|
return res
|
|
}
|
|
items := strings.Split(scope, ":")
|
|
res = append(res, &token.ResourceActions{
|
|
Type: items[0],
|
|
Name: items[1],
|
|
Actions: strings.Split(items[2], ","),
|
|
})
|
|
return res
|
|
}
|
|
|
|
//Try to modify the action list in access based on permission
|
|
//determine if the request needs to be authenticated.
|
|
//for details see:https://github.com/docker/docker/issues/15640
|
|
func FilterAccess(username string, authenticated bool, a *token.ResourceActions) {
|
|
|
|
if a.Type == "registry" && a.Name == "catalog" {
|
|
return
|
|
} else {
|
|
//clear action list to assign to new acess element after perm check.
|
|
a.Actions = []string{}
|
|
if a.Type == "repository" {
|
|
if strings.Contains(a.Name, "/") { //Only check the permission when the requested image has a namespace, i.e. project
|
|
projectName := a.Name[0:strings.LastIndex(a.Name, "/")]
|
|
var permission string
|
|
var err error
|
|
if authenticated {
|
|
if username == "admin" {
|
|
exist, err := dao.ProjectExists(projectName)
|
|
if err != nil {
|
|
log.Printf("Error occurred in CheckExistProject: %v", err)
|
|
return
|
|
}
|
|
if exist {
|
|
permission = "RW"
|
|
} else {
|
|
permission = ""
|
|
log.Printf("project %s does not exist, set empty permission for admin", projectName)
|
|
}
|
|
} else {
|
|
permission, err = dao.GetPermission(username, projectName)
|
|
if err != nil {
|
|
log.Printf("Error occurred in GetPermission: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if strings.Contains(permission, "W") {
|
|
a.Actions = append(a.Actions, "push")
|
|
}
|
|
if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) {
|
|
a.Actions = append(a.Actions, "pull")
|
|
}
|
|
}
|
|
}
|
|
log.Printf("current access, type: %s, name:%s, actions:%v \n", a.Type, a.Name, a.Actions)
|
|
}
|
|
}
|
|
|
|
//For the UI process to call, so it won't establish a https connection from UI to proxy.
|
|
func GenTokenForUI(username, service, scope string) (string, error) {
|
|
access := GetResourceActions(scope)
|
|
for _, a := range access {
|
|
FilterAccess(username, true, a)
|
|
}
|
|
return MakeToken(username, service, access)
|
|
}
|
|
|
|
func MakeToken(username, service string, access []*token.ResourceActions) (string, error) {
|
|
pk, err := libtrust.LoadKeyFile(privateKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tk, err := makeTokenCore(issuer, username, service, expiration, access, pk)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature))
|
|
return rs, nil
|
|
}
|
|
|
|
//make token core
|
|
func makeTokenCore(issuer, subject, audience string, expiration int,
|
|
access []*token.ResourceActions, signingKey libtrust.PrivateKey) (*token.Token, error) {
|
|
|
|
joseHeader := &token.Header{
|
|
Type: "JWT",
|
|
SigningAlg: "RS256",
|
|
KeyID: signingKey.KeyID(),
|
|
}
|
|
|
|
jwtID, err := randString(16)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error to generate jwt id: %s", err)
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
claimSet := &token.ClaimSet{
|
|
Issuer: issuer,
|
|
Subject: subject,
|
|
Audience: audience,
|
|
Expiration: now.Add(time.Duration(expiration) * time.Minute).Unix(),
|
|
NotBefore: now.Unix(),
|
|
IssuedAt: now.Unix(),
|
|
JWTID: jwtID,
|
|
Access: access,
|
|
}
|
|
|
|
var joseHeaderBytes, claimSetBytes []byte
|
|
|
|
if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil {
|
|
return nil, fmt.Errorf("unable to marshal jose header: %s", err)
|
|
}
|
|
if claimSetBytes, err = json.Marshal(claimSet); err != nil {
|
|
return nil, fmt.Errorf("unable to marshal claim set: %s", err)
|
|
}
|
|
|
|
encodedJoseHeader := base64UrlEncode(joseHeaderBytes)
|
|
encodedClaimSet := base64UrlEncode(claimSetBytes)
|
|
payload := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet)
|
|
|
|
var signatureBytes []byte
|
|
if signatureBytes, _, err = signingKey.Sign(strings.NewReader(payload), crypto.SHA256); err != nil {
|
|
return nil, fmt.Errorf("unable to sign jwt payload: %s", err)
|
|
}
|
|
|
|
signature := base64UrlEncode(signatureBytes)
|
|
tokenString := fmt.Sprintf("%s.%s", payload, signature)
|
|
return token.NewToken(tokenString)
|
|
}
|
|
|
|
func randString(length int) (string, error) {
|
|
const alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
rb := make([]byte, length)
|
|
_, err := rand.Read(rb)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for i, b := range rb {
|
|
rb[i] = alphanum[int(b)%len(alphanum)]
|
|
}
|
|
return string(rb), nil
|
|
}
|
|
|
|
func base64UrlEncode(b []byte) string {
|
|
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
|
|
}
|