mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 18:55:18 +01:00
Rewrite the filters with middleware mechinism
Fixes 10532,rewrite the filters with middleware mechinism Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
parent
32f226901f
commit
0453709b74
@ -128,6 +128,7 @@ func GetUnitTestConfig() map[string]interface{} {
|
||||
common.WithClair: "true",
|
||||
common.TokenServiceURL: "http://core:8080/service/token",
|
||||
common.RegistryURL: fmt.Sprintf("http://%s:5000", ipAddress),
|
||||
common.ReadOnly: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/dghubble/sling"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
@ -143,7 +142,7 @@ func handle(r *testingRequest) (*httptest.ResponseRecorder, error) {
|
||||
}
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(resp, req)
|
||||
handler.ServeHTTP(resp, req)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/server/middleware/security"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -40,7 +41,6 @@ import (
|
||||
_ "github.com/goharbor/harbor/src/core/auth/db"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/testing/apitests/apilib"
|
||||
@ -54,6 +54,7 @@ const (
|
||||
)
|
||||
|
||||
var admin, unknownUsr, testUser *usrInfo
|
||||
var handler http.Handler
|
||||
|
||||
type testapi struct {
|
||||
basePath string
|
||||
@ -92,10 +93,6 @@ func init() {
|
||||
beego.BConfig.WebConfig.Session.SessionOn = true
|
||||
beego.TestBeegoInit(apppath)
|
||||
|
||||
filter.Init()
|
||||
beego.InsertFilter("/api/*", beego.BeforeStatic, filter.SessionCheck)
|
||||
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
|
||||
|
||||
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
|
||||
beego.Router("/api/search/", &SearchAPI{})
|
||||
beego.Router("/api/projects/", &ProjectAPI{}, "get:List;post:Post;head:Head")
|
||||
@ -218,6 +215,8 @@ func init() {
|
||||
// Init mock jobservice
|
||||
mockServer := test.NewJobServiceServer()
|
||||
defer mockServer.Close()
|
||||
|
||||
handler = security.Middleware()(beego.BeeApp.Handlers)
|
||||
}
|
||||
|
||||
func request0(_sling *sling.Sling, acceptHeader string, authInfo ...usrInfo) (int, http.Header, []byte, error) {
|
||||
@ -230,7 +229,7 @@ func request0(_sling *sling.Sling, acceptHeader string, authInfo ...usrInfo) (in
|
||||
req.SetBasicAuth(authInfo[0].Name, authInfo[0].Passwd)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, req)
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
body, err := ioutil.ReadAll(w.Body)
|
||||
return w.Code, w.Header(), body, err
|
||||
|
@ -29,7 +29,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
)
|
||||
|
||||
@ -324,7 +324,7 @@ func (ua *UserAPI) Post() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ua.IsAdmin && !filter.ReqCarriesSession(ua.Ctx.Request) {
|
||||
if !ua.IsAdmin && !internal.GetCarrySession(ua.Ctx.Request.Context()) {
|
||||
ua.SendForbiddenError(errors.New("self-registration cannot be triggered via API"))
|
||||
return
|
||||
}
|
||||
|
@ -212,18 +212,16 @@ func SearchGroup(groupKey string) (*models.UserGroup, error) {
|
||||
// SearchAndOnBoardUser ... Search user and OnBoard user, if user exist, return the ID of current user.
|
||||
func SearchAndOnBoardUser(username string) (int, error) {
|
||||
user, err := SearchUser(username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if user == nil {
|
||||
return 0, ErrorUserNotExist
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if user != nil {
|
||||
err = OnBoardUser(user)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return user.UserID, nil
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@ package controllers
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/core/api"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -34,9 +33,10 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
email_util "github.com/goharbor/harbor/src/common/utils/email"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/api"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
)
|
||||
|
||||
// CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ...
|
||||
@ -60,8 +60,7 @@ type messageDetail struct {
|
||||
}
|
||||
|
||||
func redirectForOIDC(ctx context.Context, username string) bool {
|
||||
am, _ := ctx.Value(filter.AuthModeKey).(string)
|
||||
if am != common.OIDCAuth {
|
||||
if internal.GetAuthMode(ctx) != common.OIDCAuth {
|
||||
return false
|
||||
}
|
||||
u, err := dao.GetUser(models.User{Username: username})
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/core/middlewares"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@ -30,7 +31,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
utilstest "github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -88,9 +88,9 @@ func TestUserResettable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRedirectForOIDC(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), filter.AuthModeKey, common.DBAuth)
|
||||
ctx := internal.WithAuthMode(context.Background(), common.DBAuth)
|
||||
assert.False(t, redirectForOIDC(ctx, "nonexist"))
|
||||
ctx = context.WithValue(context.Background(), filter.AuthModeKey, common.OIDCAuth)
|
||||
ctx = internal.WithAuthMode(context.Background(), common.OIDCAuth)
|
||||
assert.True(t, redirectForOIDC(ctx, "nonexist"))
|
||||
assert.False(t, redirectForOIDC(ctx, "admin"))
|
||||
|
||||
|
@ -1,48 +0,0 @@
|
||||
// Copyright 2018 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 filter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
|
||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// MediaTypeFilter filters the POST request, it returns 415 if the content type of the request
|
||||
// doesn't match the preset ones.
|
||||
func MediaTypeFilter(mediaType ...string) func(*beegoctx.Context) {
|
||||
return func(ctx *beegoctx.Context) {
|
||||
filterContentType(ctx.Request, ctx.ResponseWriter, mediaType...)
|
||||
}
|
||||
}
|
||||
|
||||
func filterContentType(req *http.Request, resp http.ResponseWriter, mediaType ...string) {
|
||||
if req.Method != http.MethodPost {
|
||||
return
|
||||
}
|
||||
v := req.Header.Get("Content-Type")
|
||||
mimeType := strings.Split(v, ";")[0]
|
||||
hlog.Debugf("Mimetype of incoming request %s: %s", req.RequestURI, mimeType)
|
||||
|
||||
for _, t := range mediaType {
|
||||
if t == mimeType {
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
// 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 filter
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMediaTypeFilter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
getReq := httptest.NewRequest(http.MethodGet, "/the/path", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
filterContentType(getReq, rec, "application/json")
|
||||
assert.Equal(http.StatusOK, rec.Code)
|
||||
|
||||
postReq := httptest.NewRequest(http.MethodPost, "/the/path", nil)
|
||||
postReq.Header.Set("Content-Type", "text/html")
|
||||
rec2 := httptest.NewRecorder()
|
||||
filterContentType(postReq, rec2, "application/json")
|
||||
assert.Equal(http.StatusUnsupportedMediaType, rec2.Code)
|
||||
postReq2 := httptest.NewRequest(http.MethodPost, "/the/path", nil)
|
||||
postReq2.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
rec3 := httptest.NewRecorder()
|
||||
filterContentType(postReq2, rec3, "application/json")
|
||||
assert.Equal(http.StatusOK, rec3.Code)
|
||||
|
||||
}
|
@ -1,399 +0,0 @@
|
||||
// Copyright 2018 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 filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/api"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
secstore "github.com/goharbor/harbor/src/common/secret"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
|
||||
"github.com/goharbor/harbor/src/common/security/secret"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/pkg/authproxy"
|
||||
"github.com/goharbor/harbor/src/pkg/robot"
|
||||
pkg_token "github.com/goharbor/harbor/src/pkg/token"
|
||||
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||
)
|
||||
|
||||
// ContextValueKey for content value
|
||||
type ContextValueKey string
|
||||
|
||||
type pathMethod struct {
|
||||
path string
|
||||
method string
|
||||
}
|
||||
|
||||
const (
|
||||
// AuthModeKey is context key for auth mode
|
||||
AuthModeKey ContextValueKey = "harbor_auth_mode"
|
||||
)
|
||||
|
||||
var (
|
||||
reqCtxModifiers []ReqCtxModifier
|
||||
)
|
||||
|
||||
// Init ReqCtxMofiers list
|
||||
func Init() {
|
||||
reqCtxModifiers = []ReqCtxModifier{
|
||||
&configCtxModifier{},
|
||||
&secretReqCtxModifier{config.SecretStore},
|
||||
&oidcCliReqCtxModifier{},
|
||||
&idTokenReqCtxModifier{},
|
||||
&authProxyReqCtxModifier{},
|
||||
&robotAuthReqCtxModifier{},
|
||||
&basicAuthReqCtxModifier{},
|
||||
&sessionReqCtxModifier{},
|
||||
&unauthorizedReqCtxModifier{}}
|
||||
}
|
||||
|
||||
// SecurityFilter authenticates the request and passes a security context
|
||||
// and a project manager with it which can be used to do some authN & authZ
|
||||
func SecurityFilter(ctx *beegoctx.Context) {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
req := ctx.Request
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// add security context and project manager to request context
|
||||
for _, modifier := range reqCtxModifiers {
|
||||
if modifier.Modify(ctx) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReqCtxModifier modifies the context of request
|
||||
type ReqCtxModifier interface {
|
||||
Modify(*beegoctx.Context) bool
|
||||
}
|
||||
|
||||
// configCtxModifier populates to the configuration values to context, which are to be read by subsequent
|
||||
// filters.
|
||||
type configCtxModifier struct {
|
||||
}
|
||||
|
||||
func (c *configCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
m, err := config.AuthMode()
|
||||
if err != nil {
|
||||
log.Warningf("Failed to get auth mode, err: %v", err)
|
||||
}
|
||||
addToReqContext(ctx.Request, AuthModeKey, m)
|
||||
return false
|
||||
}
|
||||
|
||||
type secretReqCtxModifier struct {
|
||||
store *secstore.Store
|
||||
}
|
||||
|
||||
func (s *secretReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
scrt := secstore.FromRequest(ctx.Request)
|
||||
if len(scrt) == 0 {
|
||||
return false
|
||||
}
|
||||
log.Debug("got secret from request")
|
||||
|
||||
log.Debug("creating a secret security context...")
|
||||
securCtx := secret.NewSecurityContext(scrt, s.store)
|
||||
|
||||
setSecurCtx(ctx.Request, securCtx)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type robotAuthReqCtxModifier struct{}
|
||||
|
||||
func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
robotName, robotTk, ok := ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(robotName, common.RobotPrefix) {
|
||||
return false
|
||||
}
|
||||
rClaims := &robot_claim.Claim{}
|
||||
opt := pkg_token.DefaultTokenOptions()
|
||||
rtk, err := pkg_token.Parse(opt, robotTk, rClaims)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt robot token, %v", err)
|
||||
return false
|
||||
}
|
||||
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
|
||||
ctr := robot.RobotCtr
|
||||
robot, err := ctr.GetRobotAccount(rtk.Claims.(*robot_claim.Claim).TokenID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get robot %s: %v", robotName, err)
|
||||
return false
|
||||
}
|
||||
if robot == nil {
|
||||
log.Error("the token provided doesn't exist.")
|
||||
return false
|
||||
}
|
||||
if robotName != robot.Name {
|
||||
log.Errorf("failed to authenticate : %v", robotName)
|
||||
return false
|
||||
}
|
||||
if robot.Disabled {
|
||||
log.Errorf("the robot account %s is disabled", robot.Name)
|
||||
return false
|
||||
}
|
||||
log.Debug("creating robot account security context...")
|
||||
pm := config.GlobalProjectMgr
|
||||
securCtx := robotCtx.NewSecurityContext(robot, pm, rtk.Claims.(*robot_claim.Claim).Access)
|
||||
setSecurCtx(ctx.Request, securCtx)
|
||||
return true
|
||||
}
|
||||
|
||||
type oidcCliReqCtxModifier struct{}
|
||||
|
||||
func (oc *oidcCliReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
path := ctx.Request.URL.Path
|
||||
if path != "/service/token" &&
|
||||
!strings.HasPrefix(path, "/v2") &&
|
||||
!strings.HasPrefix(path, "/chartrepo/") &&
|
||||
!strings.HasPrefix(path, fmt.Sprintf("/api/%s/chartrepo/", api.APIVersion)) {
|
||||
log.Debug("OIDC CLI modifier only handles request by docker CLI or helm CLI")
|
||||
return false
|
||||
}
|
||||
if ctx.Request.Context().Value(AuthModeKey).(string) != common.OIDCAuth {
|
||||
return false
|
||||
}
|
||||
username, secret, ok := ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
user, err := oidc.VerifySecret(ctx.Request.Context(), username, secret)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to verify secret: %v", err)
|
||||
return false
|
||||
}
|
||||
pm := config.GlobalProjectMgr
|
||||
sc := local.NewSecurityContext(user, pm)
|
||||
setSecurCtx(ctx.Request, sc)
|
||||
return true
|
||||
}
|
||||
|
||||
type idTokenReqCtxModifier struct{}
|
||||
|
||||
func (it *idTokenReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
req := ctx.Request
|
||||
if req.Context().Value(AuthModeKey).(string) != common.OIDCAuth {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(ctx.Request.URL.Path, "/api") {
|
||||
return false
|
||||
}
|
||||
h := req.Header.Get("Authorization")
|
||||
token := strings.Split(h, "Bearer")
|
||||
if len(token) < 2 {
|
||||
return false
|
||||
}
|
||||
claims, err := oidc.VerifyToken(req.Context(), strings.TrimSpace(token[1]))
|
||||
if err != nil {
|
||||
log.Warningf("Failed to verify token, error: %v", err)
|
||||
return false
|
||||
}
|
||||
u, err := dao.GetUserBySubIss(claims.Subject, claims.Issuer)
|
||||
if err != nil {
|
||||
log.Warningf("Failed to get user based on token claims, error: %v", err)
|
||||
return false
|
||||
}
|
||||
if u == nil {
|
||||
log.Warning("User matches token's claims is not onboarded.")
|
||||
return false
|
||||
}
|
||||
settings, err := config.OIDCSetting()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get OIDC settings, error: %v", err)
|
||||
}
|
||||
if groupNames, ok := oidc.GroupsFromClaims(claims, settings.GroupsClaim); ok {
|
||||
groups := models.UserGroupsFromName(groupNames, common.OIDCGroupType)
|
||||
u.GroupIDs, err = group.PopulateGroup(groups)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get group ID list for OIDC user: %s, error: %v", u.Username, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
pm := config.GlobalProjectMgr
|
||||
sc := local.NewSecurityContext(u, pm)
|
||||
setSecurCtx(ctx.Request, sc)
|
||||
return true
|
||||
}
|
||||
|
||||
type authProxyReqCtxModifier struct{}
|
||||
|
||||
func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
if ctx.Request.Context().Value(AuthModeKey).(string) != common.HTTPAuth {
|
||||
return false
|
||||
}
|
||||
|
||||
// only support docker login
|
||||
if ctx.Request.URL.Path != "/service/token" {
|
||||
log.Debug("Auth proxy modifier only handles docker login request.")
|
||||
return false
|
||||
}
|
||||
|
||||
proxyUserName, proxyPwd, ok := ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
rawUserName, match := ap.matchAuthProxyUserName(proxyUserName)
|
||||
if !match {
|
||||
log.Errorf("User name %s doesn't meet the auth proxy name pattern", proxyUserName)
|
||||
return false
|
||||
}
|
||||
httpAuthProxyConf, err := config.HTTPAuthProxySetting()
|
||||
if err != nil {
|
||||
log.Errorf("fail to get auth proxy settings, %v", err)
|
||||
return false
|
||||
}
|
||||
tokenReviewStatus, err := authproxy.TokenReview(proxyPwd, httpAuthProxyConf)
|
||||
if err != nil {
|
||||
log.Errorf("fail to review token, %v", err)
|
||||
return false
|
||||
}
|
||||
if rawUserName != tokenReviewStatus.User.Username {
|
||||
log.Errorf("user name doesn't match with token: %s", rawUserName)
|
||||
return false
|
||||
}
|
||||
user, err := dao.GetUser(models.User{
|
||||
Username: rawUserName,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("fail to get user: %s, error: %v", rawUserName, err)
|
||||
return false
|
||||
}
|
||||
if user == nil { // onboard user if it's not yet onboarded.
|
||||
uid, err := auth.SearchAndOnBoardUser(rawUserName)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to search and onboard user, username: %s, error: %v", rawUserName, err)
|
||||
return false
|
||||
}
|
||||
user, err = dao.GetUser(models.User{
|
||||
UserID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Fail to get user, name: %s, ID: %d, error: %v", rawUserName, uid, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
u2, err := authproxy.UserFromReviewStatus(tokenReviewStatus)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get user information from token review status, error: %v", err)
|
||||
return false
|
||||
}
|
||||
user.GroupIDs = u2.GroupIDs
|
||||
pm := config.GlobalProjectMgr
|
||||
log.Debug("creating local database security context for auth proxy...")
|
||||
securCtx := local.NewSecurityContext(user, pm)
|
||||
setSecurCtx(ctx.Request, securCtx)
|
||||
return true
|
||||
}
|
||||
|
||||
func (ap *authProxyReqCtxModifier) matchAuthProxyUserName(name string) (string, bool) {
|
||||
if !strings.HasPrefix(name, common.AuthProxyUserNamePrefix) {
|
||||
return "", false
|
||||
}
|
||||
return strings.Replace(name, common.AuthProxyUserNamePrefix, "", -1), true
|
||||
}
|
||||
|
||||
type basicAuthReqCtxModifier struct{}
|
||||
|
||||
func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
username, password, ok := ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
log.Debug("got user information via basic auth")
|
||||
|
||||
user, err := auth.Login(models.AuthModel{
|
||||
Principal: username,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to authenticate %s: %v", username, err)
|
||||
return false
|
||||
}
|
||||
if user == nil {
|
||||
log.Debug("basic auth user is nil")
|
||||
return false
|
||||
}
|
||||
pm := config.GlobalProjectMgr
|
||||
log.Debug("creating local database security context...")
|
||||
securCtx := local.NewSecurityContext(user, pm)
|
||||
setSecurCtx(ctx.Request, securCtx)
|
||||
return true
|
||||
}
|
||||
|
||||
type sessionReqCtxModifier struct{}
|
||||
|
||||
func (s *sessionReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
userInterface := ctx.Input.Session("user")
|
||||
if userInterface == nil {
|
||||
log.Debug("can not get user information from session")
|
||||
return false
|
||||
}
|
||||
log.Debug("got user information from session")
|
||||
user, ok := userInterface.(models.User)
|
||||
if !ok {
|
||||
log.Info("can not get user information from session")
|
||||
return false
|
||||
}
|
||||
pm := config.GlobalProjectMgr
|
||||
log.Debug("creating local database security context...")
|
||||
securityCtx := local.NewSecurityContext(&user, pm)
|
||||
|
||||
setSecurCtx(ctx.Request, securityCtx)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// use this one as the last modifier in the modifier list for unauthorized request
|
||||
type unauthorizedReqCtxModifier struct{}
|
||||
|
||||
func (u *unauthorizedReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
log.Debug("user information is nil")
|
||||
pm := config.GlobalProjectMgr
|
||||
log.Debug("creating local database security context...")
|
||||
securCtx := local.NewSecurityContext(nil, pm)
|
||||
setSecurCtx(ctx.Request, securCtx)
|
||||
return true
|
||||
}
|
||||
|
||||
func setSecurCtx(req *http.Request, ctx security.Context) {
|
||||
*req = *(req.WithContext(security.NewContext(req.Context(), ctx)))
|
||||
}
|
||||
|
||||
func addToReqContext(req *http.Request, key, value interface{}) {
|
||||
*req = *(req.WithContext(context.WithValue(req.Context(), key, value)))
|
||||
}
|
@ -1,436 +0,0 @@
|
||||
// Copyright 2018 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 filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
"github.com/astaxie/beego/session"
|
||||
config2 "github.com/goharbor/harbor/src/common/config"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
commonsecret "github.com/goharbor/harbor/src/common/secret"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/security/secret"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/authproxy"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/db"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
fiter_test "github.com/goharbor/harbor/src/core/filter/test"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// initialize beego session manager
|
||||
conf := &session.ManagerConfig{
|
||||
CookieName: beego.BConfig.WebConfig.Session.SessionName,
|
||||
Gclifetime: beego.BConfig.WebConfig.Session.SessionGCMaxLifetime,
|
||||
ProviderConfig: filepath.ToSlash(beego.BConfig.WebConfig.Session.SessionProviderConfig),
|
||||
Secure: beego.BConfig.Listen.EnableHTTPS,
|
||||
EnableSetCookie: beego.BConfig.WebConfig.Session.SessionAutoSetCookie,
|
||||
Domain: beego.BConfig.WebConfig.Session.SessionDomain,
|
||||
CookieLifeTime: beego.BConfig.WebConfig.Session.SessionCookieLifeTime,
|
||||
}
|
||||
|
||||
var err error
|
||||
beego.GlobalSessions, err = session.NewManager("memory", conf)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create session manager: %v", err)
|
||||
}
|
||||
config.Init()
|
||||
test.InitDatabaseFromEnv()
|
||||
|
||||
config.Upload(test.GetUnitTestConfig())
|
||||
Init()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestSecurityFilter(t *testing.T) {
|
||||
// nil request
|
||||
ctx, err := newContext(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create context: %v", err)
|
||||
}
|
||||
SecurityFilter(ctx)
|
||||
assert.Nil(t, securityContext(ctx))
|
||||
|
||||
// the pattern of request needs security check
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
|
||||
ctx, err = newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to crate context: %v", err)
|
||||
}
|
||||
SecurityFilter(ctx)
|
||||
assert.NotNil(t, securityContext(ctx))
|
||||
}
|
||||
|
||||
func TestConfigCtxModifier(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
conf := map[string]interface{}{
|
||||
common.AUTHMode: common.OIDCAuth,
|
||||
common.OIDCName: "test",
|
||||
common.OIDCEndpoint: "https://accounts.google.com",
|
||||
common.OIDCVerifyCert: "true",
|
||||
common.OIDCScope: "openid, profile, offline_access",
|
||||
common.OIDCGroupsClaim: "groups",
|
||||
common.OIDCCLientID: "client",
|
||||
common.OIDCClientSecret: "secret",
|
||||
common.ExtEndpoint: "https://harbor.test",
|
||||
}
|
||||
config.InitWithSettings(conf)
|
||||
ctx, err := newContext(req)
|
||||
m := &configCtxModifier{}
|
||||
f := m.Modify(ctx)
|
||||
assert.False(t, f)
|
||||
assert.Equal(t, common.OIDCAuth, req.Context().Value(AuthModeKey).(string))
|
||||
}
|
||||
|
||||
func TestSecretReqCtxModifier(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
commonsecret.AddToRequest(req, "secret")
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to crate context: %v", err)
|
||||
}
|
||||
|
||||
modifier := &secretReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
assert.True(t, modified)
|
||||
assert.IsType(t, &secret.SecurityContext{},
|
||||
securityContext(ctx))
|
||||
}
|
||||
|
||||
func TestOIDCCliReqCtxModifier(t *testing.T) {
|
||||
conf := map[string]interface{}{
|
||||
common.AUTHMode: common.OIDCAuth,
|
||||
common.OIDCName: "test",
|
||||
common.OIDCEndpoint: "https://accounts.google.com",
|
||||
common.OIDCVerifyCert: "true",
|
||||
common.OIDCScope: "openid, profile, offline_access",
|
||||
common.OIDCCLientID: "client",
|
||||
common.OIDCClientSecret: "secret",
|
||||
common.ExtEndpoint: "https://harbor.test",
|
||||
}
|
||||
|
||||
kp := &config2.PresetKeyProvider{Key: "naa4JtarA1Zsc3uY"}
|
||||
config.InitWithSettings(conf, kp)
|
||||
|
||||
modifier := &oidcCliReqCtxModifier{}
|
||||
req1, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
ctx1, err := newContext(req1)
|
||||
require.Nil(t, err)
|
||||
assert.False(t, modifier.Modify(ctx1))
|
||||
req2, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/service/token", nil)
|
||||
require.Nil(t, err)
|
||||
addToReqContext(req2, AuthModeKey, common.OIDCAuth)
|
||||
ctx2, err := newContext(req2)
|
||||
require.Nil(t, err)
|
||||
assert.False(t, modifier.Modify(ctx2))
|
||||
username := "oidcModiferTester"
|
||||
password := "oidcSecret"
|
||||
u := &models.User{
|
||||
Username: username,
|
||||
Email: "testtest@test.org",
|
||||
Password: "12345678",
|
||||
}
|
||||
id, err := dao.Register(*u)
|
||||
require.Nil(t, err)
|
||||
oidc.SetHardcodeVerifierForTest(password)
|
||||
req3, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/service/token", nil)
|
||||
require.Nil(t, err)
|
||||
req3.SetBasicAuth(username, password)
|
||||
addToReqContext(req3, AuthModeKey, common.OIDCAuth)
|
||||
ctx3, err := newContext(req3)
|
||||
assert.True(t, modifier.Modify(ctx3))
|
||||
o := dao.GetOrmer()
|
||||
_, err = o.Delete(&models.User{UserID: int(id)})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestIdTokenReqCtxModifier(t *testing.T) {
|
||||
bc := context.Background()
|
||||
it := &idTokenReqCtxModifier{}
|
||||
r1, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/chartrepo/", nil)
|
||||
require.Nil(t, err)
|
||||
req1 := r1.WithContext(context.WithValue(bc, AuthModeKey, common.DBAuth))
|
||||
ctx1, err := newContext(req1)
|
||||
require.Nil(t, err)
|
||||
assert.False(t, it.Modify(ctx1))
|
||||
|
||||
req2 := r1.WithContext(context.WithValue(bc, AuthModeKey, common.OIDCAuth))
|
||||
ctx2, err := newContext(req2)
|
||||
require.Nil(t, err)
|
||||
assert.False(t, it.Modify(ctx2))
|
||||
|
||||
r2, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
req3 := r2.WithContext(context.WithValue(bc, AuthModeKey, common.OIDCAuth))
|
||||
ctx3, err := newContext(req3)
|
||||
require.Nil(t, err)
|
||||
assert.False(t, it.Modify(ctx3))
|
||||
}
|
||||
|
||||
func TestRobotReqCtxModifier(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
req.SetBasicAuth("robot$test1", "Harbor12345")
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to crate context: %v", err)
|
||||
}
|
||||
|
||||
modifier := &robotAuthReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
assert.False(t, modified)
|
||||
}
|
||||
|
||||
func TestAuthProxyReqCtxModifier(t *testing.T) {
|
||||
|
||||
server, err := fiter_test.NewAuthProxyTestServer()
|
||||
assert.Nil(t, err)
|
||||
defer server.Close()
|
||||
|
||||
c := map[string]interface{}{
|
||||
common.HTTPAuthProxySkipSearch: "true",
|
||||
common.HTTPAuthProxyVerifyCert: "false",
|
||||
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
|
||||
common.HTTPAuthProxyTokenReviewEndpoint: server.URL,
|
||||
common.AUTHMode: common.HTTPAuth,
|
||||
}
|
||||
|
||||
config.Upload(c)
|
||||
v, e := config.HTTPAuthProxySetting()
|
||||
assert.Nil(t, e)
|
||||
assert.Equal(t, *v, models.HTTPAuthProxy{
|
||||
Endpoint: "https://auth.proxy/suffix",
|
||||
SkipSearch: true,
|
||||
VerifyCert: false,
|
||||
TokenReviewEndpoint: server.URL,
|
||||
})
|
||||
|
||||
// No onboard
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/service/token", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
req.SetBasicAuth("tokenreview$administrator@vsphere.local", "reviEwt0k3n")
|
||||
addToReqContext(req, AuthModeKey, common.HTTPAuth)
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create context: %v", err)
|
||||
}
|
||||
|
||||
modifier := &authProxyReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
assert.True(t, modified)
|
||||
|
||||
}
|
||||
|
||||
func TestBasicAuthReqCtxModifier(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
req.SetBasicAuth("admin", "Harbor12345")
|
||||
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to crate context: %v", err)
|
||||
}
|
||||
|
||||
modifier := &basicAuthReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
assert.True(t, modified)
|
||||
|
||||
sc := securityContext(ctx)
|
||||
assert.IsType(t, &local.SecurityContext{}, sc)
|
||||
s := sc.(security.Context)
|
||||
assert.Equal(t, "admin", s.GetUsername())
|
||||
}
|
||||
|
||||
func TestSessionReqCtxModifier(t *testing.T) {
|
||||
user := models.User{
|
||||
Username: "admin",
|
||||
UserID: 1,
|
||||
Email: "admin@example.com",
|
||||
SysAdminFlag: true,
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
store, err := beego.GlobalSessions.SessionStart(httptest.NewRecorder(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create session store: %v", err)
|
||||
}
|
||||
if err = store.Set("user", user); err != nil {
|
||||
t.Fatalf("failed to set session: %v", err)
|
||||
}
|
||||
|
||||
addSessionIDToCookie(req, store.SessionID())
|
||||
addToReqContext(req, AuthModeKey, common.DBAuth)
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create context: %v", err)
|
||||
}
|
||||
|
||||
modifier := &sessionReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
|
||||
assert.True(t, modified)
|
||||
sc := securityContext(ctx)
|
||||
assert.IsType(t, &local.SecurityContext{}, sc)
|
||||
s := sc.(security.Context)
|
||||
assert.Equal(t, "admin", s.GetUsername())
|
||||
assert.True(t, s.IsSysAdmin())
|
||||
}
|
||||
|
||||
func TestSessionReqCtxModifierFailed(t *testing.T) {
|
||||
user := "admin"
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
store, err := beego.GlobalSessions.SessionStart(httptest.NewRecorder(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create session store: %v", err)
|
||||
}
|
||||
if err = store.Set("user", user); err != nil {
|
||||
t.Fatalf("failed to set session: %v", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
addSessionIDToCookie(req, store.SessionID())
|
||||
addToReqContext(req, AuthModeKey, common.DBAuth)
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to crate context: %v", err)
|
||||
}
|
||||
modifier := &sessionReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
|
||||
assert.False(t, modified)
|
||||
|
||||
}
|
||||
|
||||
// TODO add test case
|
||||
func TestTokenReqCtxModifier(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestUnauthorizedReqCtxModifier(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet,
|
||||
"http://127.0.0.1/api/projects/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", req)
|
||||
}
|
||||
|
||||
ctx, err := newContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to crate context: %v", err)
|
||||
}
|
||||
|
||||
modifier := &unauthorizedReqCtxModifier{}
|
||||
modified := modifier.Modify(ctx)
|
||||
assert.True(t, modified)
|
||||
|
||||
sc := securityContext(ctx)
|
||||
assert.NotNil(t, sc)
|
||||
s := sc.(security.Context)
|
||||
assert.False(t, s.IsAuthenticated())
|
||||
}
|
||||
|
||||
func newContext(req *http.Request) (*beegoctx.Context, error) {
|
||||
var err error
|
||||
ctx := beegoctx.NewContext()
|
||||
ctx.Reset(httptest.NewRecorder(), req)
|
||||
if req != nil {
|
||||
ctx.Input.CruSession, err = beego.GlobalSessions.SessionStart(ctx.ResponseWriter, req)
|
||||
}
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
func addSessionIDToCookie(req *http.Request, sessionID string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: beego.BConfig.WebConfig.Session.SessionName,
|
||||
Value: url.QueryEscape(sessionID),
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: beego.BConfig.Listen.EnableHTTPS,
|
||||
Domain: beego.BConfig.WebConfig.Session.SessionDomain,
|
||||
}
|
||||
cookie.MaxAge = beego.BConfig.WebConfig.Session.SessionCookieLifeTime
|
||||
cookie.Expires = time.Now().Add(
|
||||
time.Duration(
|
||||
beego.BConfig.WebConfig.Session.SessionCookieLifeTime) * time.Second)
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
|
||||
func securityContext(ctx *beegoctx.Context) interface{} {
|
||||
if ctx.Request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, ok := security.FromContext(ctx.Request.Context())
|
||||
if !ok {
|
||||
log.Printf("failed to get security context")
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SessionReqKey is the key in the context of a request to mark the request carries session when reaching the backend
|
||||
const SessionReqKey ContextValueKey = "harbor_with_session_req"
|
||||
|
||||
// SessionCheck is a filter to mark the requests that carries a session id, it has to be registered as
|
||||
// "beego.BeforeStatic" because beego will modify the request after execution of these filters, all requests will
|
||||
// appear to have a session id cookie.
|
||||
func SessionCheck(ctx *beegoctx.Context) {
|
||||
req := ctx.Request
|
||||
_, err := req.Cookie(config.SessionCookieName)
|
||||
if err == nil {
|
||||
ctx.Request = req.WithContext(context.WithValue(req.Context(), SessionReqKey, true))
|
||||
log.Debugf("Mark the request as with-session: %s %s", req.Method, req.URL.RawPath)
|
||||
}
|
||||
}
|
||||
|
||||
// ReqCarriesSession verifies if the request carries session when
|
||||
func ReqCarriesSession(req *http.Request) bool {
|
||||
r, ok := req.Context().Value(SessionReqKey).(bool)
|
||||
return ok && r
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReqHasNoSession(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "https://127.0.0.1:8080/api/users", nil)
|
||||
ctx := beegoctx.NewContext()
|
||||
ctx.Request = req
|
||||
SessionCheck(ctx)
|
||||
assert.False(t, ReqCarriesSession(ctx.Request))
|
||||
}
|
@ -38,7 +38,6 @@ import (
|
||||
_ "github.com/goharbor/harbor/src/core/auth/oidc"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/uaa"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/src/core/middlewares"
|
||||
"github.com/goharbor/harbor/src/core/service/token"
|
||||
"github.com/goharbor/harbor/src/migration"
|
||||
@ -156,10 +155,6 @@ func main() {
|
||||
log.Info("initializing notification...")
|
||||
notification.Init()
|
||||
|
||||
filter.Init()
|
||||
beego.InsertFilter("/api/*", beego.BeforeStatic, filter.SessionCheck)
|
||||
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
|
||||
|
||||
server.RegisterRoutes()
|
||||
|
||||
if common_http.InternalTLSEnabled() {
|
||||
|
@ -15,6 +15,9 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/server/middleware/csrf"
|
||||
"github.com/goharbor/harbor/src/server/middleware/log"
|
||||
"github.com/goharbor/harbor/src/server/middleware/requestid"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
@ -23,12 +26,11 @@ import (
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"github.com/goharbor/harbor/src/server/middleware/csrf"
|
||||
"github.com/goharbor/harbor/src/server/middleware/log"
|
||||
"github.com/goharbor/harbor/src/server/middleware/notification"
|
||||
"github.com/goharbor/harbor/src/server/middleware/orm"
|
||||
"github.com/goharbor/harbor/src/server/middleware/readonly"
|
||||
"github.com/goharbor/harbor/src/server/middleware/requestid"
|
||||
"github.com/goharbor/harbor/src/server/middleware/security"
|
||||
"github.com/goharbor/harbor/src/server/middleware/session"
|
||||
"github.com/goharbor/harbor/src/server/middleware/transaction"
|
||||
)
|
||||
|
||||
@ -74,9 +76,11 @@ func legacyAPISkipper(r *http.Request) bool {
|
||||
// MiddleWares returns global middlewares
|
||||
func MiddleWares() []beego.MiddleWare {
|
||||
return []beego.MiddleWare{
|
||||
csrf.Middleware(),
|
||||
requestid.Middleware(),
|
||||
log.Middleware(),
|
||||
session.Middleware(),
|
||||
csrf.Middleware(),
|
||||
security.Middleware(),
|
||||
readonly.Middleware(readonlySkippers...),
|
||||
orm.Middleware(legacyAPISkipper),
|
||||
// notification must ahead of transaction ensure the DB transaction execution complete
|
||||
|
@ -22,6 +22,8 @@ type contextKey string
|
||||
const (
|
||||
contextKeyAPIVersion contextKey = "apiVersion"
|
||||
contextKeyArtifactInfo contextKey = "artifactInfo"
|
||||
contextKeyAuthMode contextKey = "authMode"
|
||||
contextKeyCarrySession contextKey = "carrySession"
|
||||
)
|
||||
|
||||
// ArtifactInfo wraps the artifact info extracted from the request to "/v2/"
|
||||
@ -60,7 +62,7 @@ func GetAPIVersion(ctx context.Context) string {
|
||||
version := ""
|
||||
value := getFromContext(ctx, contextKeyAPIVersion)
|
||||
if value != nil {
|
||||
version = value.(string)
|
||||
version, _ = value.(string)
|
||||
}
|
||||
return version
|
||||
}
|
||||
@ -74,7 +76,37 @@ func WithArtifactInfo(ctx context.Context, art ArtifactInfo) context.Context {
|
||||
func GetArtifactInfo(ctx context.Context) (art ArtifactInfo) {
|
||||
value := getFromContext(ctx, contextKeyArtifactInfo)
|
||||
if value != nil {
|
||||
art = value.(ArtifactInfo)
|
||||
art, _ = value.(ArtifactInfo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WithAuthMode returns a context with auth mode set
|
||||
func WithAuthMode(ctx context.Context, mode string) context.Context {
|
||||
return setToContext(ctx, contextKeyAuthMode, mode)
|
||||
}
|
||||
|
||||
// GetAuthMode gets the auth mode from the context
|
||||
func GetAuthMode(ctx context.Context) string {
|
||||
mode := ""
|
||||
value := getFromContext(ctx, contextKeyAuthMode)
|
||||
if value != nil {
|
||||
mode, _ = value.(string)
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// WithCarrySession returns a context with "carry session" set that indicates whether the request carries session or not
|
||||
func WithCarrySession(ctx context.Context, carrySession bool) context.Context {
|
||||
return setToContext(ctx, contextKeyCarrySession, carrySession)
|
||||
}
|
||||
|
||||
// GetCarrySession gets the "carry session" from the context indicates whether the request carries session or not
|
||||
func GetCarrySession(ctx context.Context) bool {
|
||||
carrySession := false
|
||||
value := getFromContext(ctx, contextKeyCarrySession)
|
||||
if value != nil {
|
||||
carrySession, _ = value.(bool)
|
||||
}
|
||||
return carrySession
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
serror "github.com/goharbor/harbor/src/server/error"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
@ -78,14 +79,10 @@ func Middleware() func(handler http.Handler) http.Handler {
|
||||
// csrfSkipper makes sure only some of the uris accessed by non-UI client can skip the csrf check
|
||||
func csrfSkipper(req *http.Request) bool {
|
||||
path := req.URL.Path
|
||||
// We can check the cookie directly b/c the filter and controllerRegistry is executed after middleware, so no session
|
||||
// cookie is added by beego.
|
||||
_, err := req.Cookie(config.SessionCookieName)
|
||||
hasSession := err == nil
|
||||
if (strings.HasPrefix(path, "/v2/") ||
|
||||
strings.HasPrefix(path, "/api/") ||
|
||||
strings.HasPrefix(path, "/chartrepo/") ||
|
||||
strings.HasPrefix(path, "/service/")) && !hasSession {
|
||||
strings.HasPrefix(path, "/service/")) && !internal.GetCarrySession(req.Context()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
104
src/server/middleware/security/auth_proxy.go
Normal file
104
src/server/middleware/security/auth_proxy.go
Normal file
@ -0,0 +1,104 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"github.com/goharbor/harbor/src/pkg/authproxy"
|
||||
)
|
||||
|
||||
type authProxy struct{}
|
||||
|
||||
func (a *authProxy) Generate(req *http.Request) security.Context {
|
||||
log := log.G(req.Context())
|
||||
if internal.GetAuthMode(req.Context()) != common.HTTPAuth {
|
||||
return nil
|
||||
}
|
||||
// only support docker login
|
||||
if req.URL.Path != "/service/token" {
|
||||
return nil
|
||||
}
|
||||
proxyUserName, proxyPwd, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
rawUserName, match := a.matchAuthProxyUserName(proxyUserName)
|
||||
if !match {
|
||||
log.Errorf("user name %s doesn't meet the auth proxy name pattern", proxyUserName)
|
||||
return nil
|
||||
}
|
||||
httpAuthProxyConf, err := config.HTTPAuthProxySetting()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get auth proxy settings: %v", err)
|
||||
return nil
|
||||
}
|
||||
tokenReviewStatus, err := authproxy.TokenReview(proxyPwd, httpAuthProxyConf)
|
||||
if err != nil {
|
||||
log.Errorf("failed to review token: %v", err)
|
||||
return nil
|
||||
}
|
||||
if rawUserName != tokenReviewStatus.User.Username {
|
||||
log.Errorf("user name doesn't match with token: %s", rawUserName)
|
||||
return nil
|
||||
}
|
||||
user, err := dao.GetUser(models.User{
|
||||
Username: rawUserName,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to get user %s: %v", rawUserName, err)
|
||||
return nil
|
||||
}
|
||||
if user == nil {
|
||||
// onboard user if it's not yet onboarded.
|
||||
uid, err := auth.SearchAndOnBoardUser(rawUserName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to search and onboard user %s: %v", rawUserName, err)
|
||||
return nil
|
||||
}
|
||||
user, err = dao.GetUser(models.User{
|
||||
UserID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to get user, name: %s, ID: %d: %v", rawUserName, uid, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
u2, err := authproxy.UserFromReviewStatus(tokenReviewStatus)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get user information from token review status: %v", err)
|
||||
return nil
|
||||
}
|
||||
user.GroupIDs = u2.GroupIDs
|
||||
log.Debugf("an auth proxy security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return local.NewSecurityContext(user, config.GlobalProjectMgr)
|
||||
}
|
||||
|
||||
func (a *authProxy) matchAuthProxyUserName(name string) (string, bool) {
|
||||
if !strings.HasPrefix(name, common.AuthProxyUserNamePrefix) {
|
||||
return "", false
|
||||
}
|
||||
return strings.Replace(name, common.AuthProxyUserNamePrefix, "", -1), true
|
||||
}
|
@ -1,17 +1,75 @@
|
||||
package test
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/authproxy"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"k8s.io/api/authentication/v1beta1"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAuthProxy(t *testing.T) {
|
||||
config.Init()
|
||||
test.InitDatabaseFromEnv()
|
||||
authProxy := &authProxy{}
|
||||
|
||||
server, err := newAuthProxyTestServer()
|
||||
require.Nil(t, err)
|
||||
defer server.Close()
|
||||
|
||||
c := map[string]interface{}{
|
||||
common.HTTPAuthProxySkipSearch: "true",
|
||||
common.HTTPAuthProxyVerifyCert: "false",
|
||||
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
|
||||
common.HTTPAuthProxyTokenReviewEndpoint: server.URL,
|
||||
common.AUTHMode: common.HTTPAuth,
|
||||
}
|
||||
config.Upload(c)
|
||||
v, e := config.HTTPAuthProxySetting()
|
||||
require.Nil(t, e)
|
||||
assert.Equal(t, *v, models.HTTPAuthProxy{
|
||||
Endpoint: "https://auth.proxy/suffix",
|
||||
SkipSearch: true,
|
||||
VerifyCert: false,
|
||||
TokenReviewEndpoint: server.URL,
|
||||
})
|
||||
|
||||
// No onboard
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/service/token", nil)
|
||||
require.Nil(t, err)
|
||||
req = req.WithContext(internal.WithAuthMode(req.Context(), common.HTTPAuth))
|
||||
req.SetBasicAuth("tokenreview$administrator@vsphere.local", "reviEwt0k3n")
|
||||
ctx := authProxy.Generate(req)
|
||||
assert.NotNil(t, ctx)
|
||||
}
|
||||
|
||||
// NewAuthProxyTestServer mocks a https server for auth proxy.
|
||||
func NewAuthProxyTestServer() (*httptest.Server, error) {
|
||||
func newAuthProxyTestServer() (*httptest.Server, error) {
|
||||
const webhookPath = "/authproxy/tokenreview"
|
||||
|
||||
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
|
50
src/server/middleware/security/basic_auth.go
Normal file
50
src/server/middleware/security/basic_auth.go
Normal file
@ -0,0 +1,50 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
type basicAuth struct{}
|
||||
|
||||
func (b *basicAuth) Generate(req *http.Request) security.Context {
|
||||
log := log.G(req.Context())
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
user, err := auth.Login(models.AuthModel{
|
||||
Principal: username,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to authenticate %s: %v", username, err)
|
||||
return nil
|
||||
}
|
||||
if user == nil {
|
||||
log.Debug("basic auth user is nil")
|
||||
return nil
|
||||
}
|
||||
log.Debugf("a basic auth security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return local.NewSecurityContext(user, config.GlobalProjectMgr)
|
||||
}
|
32
src/server/middleware/security/basic_auth_test.go
Normal file
32
src/server/middleware/security/basic_auth_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
_ "github.com/goharbor/harbor/src/core/auth/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
basicAuth := &basicAuth{}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
req.SetBasicAuth("admin", "Harbor12345")
|
||||
ctx := basicAuth.Generate(req)
|
||||
assert.NotNil(t, ctx)
|
||||
}
|
77
src/server/middleware/security/idtoken.go
Normal file
77
src/server/middleware/security/idtoken.go
Normal file
@ -0,0 +1,77 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
)
|
||||
|
||||
type idToken struct{}
|
||||
|
||||
func (i *idToken) Generate(req *http.Request) security.Context {
|
||||
log := log.G(req.Context())
|
||||
if internal.GetAuthMode(req.Context()) != common.OIDCAuth {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasPrefix(req.URL.Path, "/api") {
|
||||
return nil
|
||||
}
|
||||
h := req.Header.Get("Authorization")
|
||||
token := strings.Split(h, "Bearer")
|
||||
if len(token) < 2 {
|
||||
return nil
|
||||
}
|
||||
claims, err := oidc.VerifyToken(req.Context(), strings.TrimSpace(token[1]))
|
||||
if err != nil {
|
||||
log.Warningf("failed to verify token: %v", err)
|
||||
return nil
|
||||
}
|
||||
u, err := dao.GetUserBySubIss(claims.Subject, claims.Issuer)
|
||||
if err != nil {
|
||||
log.Warningf("failed to get user based on token claims: %v", err)
|
||||
return nil
|
||||
}
|
||||
if u == nil {
|
||||
log.Warning("user matches token's claims is not onboarded.")
|
||||
return nil
|
||||
}
|
||||
settings, err := config.OIDCSetting()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get OIDC settings: %v", err)
|
||||
return nil
|
||||
}
|
||||
if groupNames, ok := oidc.GroupsFromClaims(claims, settings.GroupsClaim); ok {
|
||||
groups := models.UserGroupsFromName(groupNames, common.OIDCGroupType)
|
||||
u.GroupIDs, err = group.PopulateGroup(groups)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get group ID list for OIDC user %s: %v", u.Username, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
log.Debugf("an ID token security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return local.NewSecurityContext(u, config.GlobalProjectMgr)
|
||||
}
|
48
src/server/middleware/security/idtoken_test.go
Normal file
48
src/server/middleware/security/idtoken_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIDToken(t *testing.T) {
|
||||
idToken := &idToken{}
|
||||
|
||||
// not the OIDC mode
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
ctx := idToken.Generate(req)
|
||||
assert.Nil(t, ctx)
|
||||
|
||||
// not the candidate request
|
||||
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1/chartrepo/", nil)
|
||||
require.Nil(t, err)
|
||||
req = req.WithContext(internal.WithAuthMode(req.Context(), common.DBAuth))
|
||||
ctx = idToken.Generate(req)
|
||||
assert.Nil(t, ctx)
|
||||
|
||||
// contains no authorization header
|
||||
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
req = req.WithContext(internal.WithAuthMode(req.Context(), common.OIDCAuth))
|
||||
ctx = idToken.Generate(req)
|
||||
assert.Nil(t, ctx)
|
||||
}
|
58
src/server/middleware/security/oidc_cli.go
Normal file
58
src/server/middleware/security/oidc_cli.go
Normal file
@ -0,0 +1,58 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/api"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
)
|
||||
|
||||
type oidcCli struct{}
|
||||
|
||||
func (o *oidcCli) Generate(req *http.Request) security.Context {
|
||||
log := log.G(req.Context())
|
||||
path := req.URL.Path
|
||||
// only handles request by docker CLI or helm CLI
|
||||
if path != "/service/token" &&
|
||||
!strings.HasPrefix(path, "/v2") &&
|
||||
!strings.HasPrefix(path, "/chartrepo/") &&
|
||||
!strings.HasPrefix(path, fmt.Sprintf("/api/%s/chartrepo/", api.APIVersion)) {
|
||||
return nil
|
||||
}
|
||||
if internal.GetAuthMode(req.Context()) != common.OIDCAuth {
|
||||
return nil
|
||||
}
|
||||
username, secret, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
user, err := oidc.VerifySecret(req.Context(), username, secret)
|
||||
if err != nil {
|
||||
log.Errorf("failed to verify secret: %v", err)
|
||||
return nil
|
||||
}
|
||||
log.Debugf("an OIDC CLI security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return local.NewSecurityContext(user, config.GlobalProjectMgr)
|
||||
}
|
49
src/server/middleware/security/oidc_cli_test.go
Normal file
49
src/server/middleware/security/oidc_cli_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOIDCCli(t *testing.T) {
|
||||
oidcCli := &oidcCli{}
|
||||
// not the candidate request
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
ctx := oidcCli.Generate(req)
|
||||
assert.Nil(t, ctx)
|
||||
|
||||
// the auth mode isn't OIDC
|
||||
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1/service/token", nil)
|
||||
require.Nil(t, err)
|
||||
ctx = oidcCli.Generate(req)
|
||||
assert.Nil(t, ctx)
|
||||
|
||||
// pass
|
||||
username := "oidcModiferTester"
|
||||
password := "oidcSecret"
|
||||
oidc.SetHardcodeVerifierForTest(password)
|
||||
req = req.WithContext(internal.WithAuthMode(req.Context(), common.OIDCAuth))
|
||||
req.SetBasicAuth(username, password)
|
||||
ctx = oidcCli.Generate(req)
|
||||
assert.NotNil(t, ctx)
|
||||
}
|
70
src/server/middleware/security/robot.go
Normal file
70
src/server/middleware/security/robot.go
Normal file
@ -0,0 +1,70 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
pkgrobot "github.com/goharbor/harbor/src/pkg/robot"
|
||||
pkg_token "github.com/goharbor/harbor/src/pkg/token"
|
||||
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
|
||||
)
|
||||
|
||||
type robot struct{}
|
||||
|
||||
func (r *robot) Generate(req *http.Request) security.Context {
|
||||
log := log.G(req.Context())
|
||||
robotName, robotTk, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasPrefix(robotName, common.RobotPrefix) {
|
||||
return nil
|
||||
}
|
||||
rClaims := &robot_claim.Claim{}
|
||||
opt := pkg_token.DefaultTokenOptions()
|
||||
rtk, err := pkg_token.Parse(opt, robotTk, rClaims)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt robot token: %v", err)
|
||||
return nil
|
||||
}
|
||||
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
|
||||
ctr := pkgrobot.RobotCtr
|
||||
robot, err := ctr.GetRobotAccount(rtk.Claims.(*robot_claim.Claim).TokenID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get robot %s: %v", robotName, err)
|
||||
return nil
|
||||
}
|
||||
if robot == nil {
|
||||
log.Error("the token provided doesn't exist.")
|
||||
return nil
|
||||
}
|
||||
if robotName != robot.Name {
|
||||
log.Errorf("failed to authenticate : %v", robotName)
|
||||
return nil
|
||||
}
|
||||
if robot.Disabled {
|
||||
log.Errorf("the robot account %s is disabled", robot.Name)
|
||||
return nil
|
||||
}
|
||||
log.Debugf("a robot security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return robotCtx.NewSecurityContext(robot, config.GlobalProjectMgr, rtk.Claims.(*robot_claim.Claim).Access)
|
||||
}
|
31
src/server/middleware/security/robot_test.go
Normal file
31
src/server/middleware/security/robot_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRobot(t *testing.T) {
|
||||
robot := &robot{}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
req.SetBasicAuth("robot$test1", "Harbor12345")
|
||||
ctx := robot.Generate(req)
|
||||
assert.Nil(t, ctx)
|
||||
}
|
37
src/server/middleware/security/secret.go
Normal file
37
src/server/middleware/security/secret.go
Normal file
@ -0,0 +1,37 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
commonsecret "github.com/goharbor/harbor/src/common/secret"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
securitysecret "github.com/goharbor/harbor/src/common/security/secret"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
type secret struct{}
|
||||
|
||||
func (s *secret) Generate(req *http.Request) security.Context {
|
||||
log := log.G(req.Context())
|
||||
sec := commonsecret.FromRequest(req)
|
||||
if len(sec) == 0 {
|
||||
return nil
|
||||
}
|
||||
log.Debugf("a secret security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return securitysecret.NewSecurityContext(sec, config.SecretStore)
|
||||
}
|
38
src/server/middleware/security/secret_test.go
Normal file
38
src/server/middleware/security/secret_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
commonsecret "github.com/goharbor/harbor/src/common/secret"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSecret(t *testing.T) {
|
||||
secret := secret{}
|
||||
|
||||
// contains no secret
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
ctx := secret.Generate(req)
|
||||
assert.Nil(t, ctx)
|
||||
|
||||
// contains secret
|
||||
commonsecret.AddToRequest(req, "secret")
|
||||
ctx = secret.Generate(req)
|
||||
assert.NotNil(t, ctx)
|
||||
}
|
64
src/server/middleware/security/security.go
Normal file
64
src/server/middleware/security/security.go
Normal file
@ -0,0 +1,64 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
generators = []generator{
|
||||
&secret{},
|
||||
&oidcCli{},
|
||||
&idToken{},
|
||||
&authProxy{},
|
||||
&robot{},
|
||||
&basicAuth{},
|
||||
&session{},
|
||||
&unauthorized{},
|
||||
}
|
||||
)
|
||||
|
||||
// security context generator
|
||||
type generator interface {
|
||||
Generate(req *http.Request) security.Context
|
||||
}
|
||||
|
||||
// Middleware returns a security context middleware that populates the security context into the request context
|
||||
func Middleware() func(http.Handler) http.Handler {
|
||||
return func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := log.G(r.Context())
|
||||
mode, err := config.AuthMode()
|
||||
if err == nil {
|
||||
r = r.WithContext(internal.WithAuthMode(r.Context(), mode))
|
||||
} else {
|
||||
log.Warningf("failed to get auth mode: %v", err)
|
||||
}
|
||||
for _, generator := range generators {
|
||||
if ctx := generator.Generate(r); ctx != nil {
|
||||
r = r.WithContext(security.NewContext(r.Context(), ctx))
|
||||
break
|
||||
}
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
37
src/server/middleware/security/security_test.go
Normal file
37
src/server/middleware/security/security_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSecurity(t *testing.T) {
|
||||
var ctx security.Context
|
||||
var exist bool
|
||||
generators = []generator{&unauthorized{}}
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, exist = security.FromContext(r.Context())
|
||||
})
|
||||
req, err := http.NewRequest("POST", "http://127.0.0.1:8080/api/users", nil)
|
||||
require.Nil(t, err)
|
||||
Middleware()(handler).ServeHTTP(nil, req)
|
||||
require.True(t, exist)
|
||||
assert.NotNil(t, ctx)
|
||||
}
|
49
src/server/middleware/security/session.go
Normal file
49
src/server/middleware/security/session.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
type session struct{}
|
||||
|
||||
func (s *session) Generate(req *http.Request) security.Context {
|
||||
log := log.G(req.Context())
|
||||
store, err := beego.GlobalSessions.SessionStart(httptest.NewRecorder(), req)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get the session store for request: %v", err)
|
||||
return nil
|
||||
}
|
||||
userInterface := store.Get("user")
|
||||
if userInterface == nil {
|
||||
return nil
|
||||
}
|
||||
user, ok := userInterface.(models.User)
|
||||
if !ok {
|
||||
log.Warning("can not convert the user in session to user model")
|
||||
return nil
|
||||
}
|
||||
log.Debugf("a session security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return local.NewSecurityContext(&user, config.GlobalProjectMgr)
|
||||
}
|
60
src/server/middleware/security/session_test.go
Normal file
60
src/server/middleware/security/session_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"github.com/astaxie/beego"
|
||||
beegosession "github.com/astaxie/beego/session"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
var err error
|
||||
// initialize beego session manager
|
||||
conf := &beegosession.ManagerConfig{
|
||||
CookieName: beego.BConfig.WebConfig.Session.SessionName,
|
||||
Gclifetime: beego.BConfig.WebConfig.Session.SessionGCMaxLifetime,
|
||||
ProviderConfig: filepath.ToSlash(beego.BConfig.WebConfig.Session.SessionProviderConfig),
|
||||
Secure: beego.BConfig.Listen.EnableHTTPS,
|
||||
EnableSetCookie: beego.BConfig.WebConfig.Session.SessionAutoSetCookie,
|
||||
Domain: beego.BConfig.WebConfig.Session.SessionDomain,
|
||||
CookieLifeTime: beego.BConfig.WebConfig.Session.SessionCookieLifeTime,
|
||||
}
|
||||
beego.GlobalSessions, err = beegosession.NewManager("memory", conf)
|
||||
require.Nil(t, err)
|
||||
|
||||
user := models.User{
|
||||
Username: "admin",
|
||||
UserID: 1,
|
||||
Email: "admin@example.com",
|
||||
SysAdminFlag: true,
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
store, err := beego.GlobalSessions.SessionStart(httptest.NewRecorder(), req)
|
||||
require.Nil(t, err)
|
||||
err = store.Set("user", user)
|
||||
require.Nil(t, err)
|
||||
|
||||
session := &session{}
|
||||
ctx := session.Generate(req)
|
||||
assert.NotNil(t, ctx)
|
||||
}
|
31
src/server/middleware/security/unauthorized.go
Normal file
31
src/server/middleware/security/unauthorized.go
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
type unauthorized struct{}
|
||||
|
||||
func (u *unauthorized) Generate(req *http.Request) security.Context {
|
||||
log.G(req.Context()).Debugf("an unauthorized security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return local.NewSecurityContext(nil, config.GlobalProjectMgr)
|
||||
}
|
30
src/server/middleware/security/unauthorized_test.go
Normal file
30
src/server/middleware/security/unauthorized_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnauthorized(t *testing.T) {
|
||||
unauthorized := &unauthorized{}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
|
||||
require.Nil(t, err)
|
||||
ctx := unauthorized.Generate(req)
|
||||
assert.NotNil(t, ctx)
|
||||
}
|
37
src/server/middleware/session/session.go
Normal file
37
src/server/middleware/session/session.go
Normal file
@ -0,0 +1,37 @@
|
||||
// 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 session
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Middleware returns a session middleware that populates the information indicates whether
|
||||
// the request carries session or not
|
||||
func Middleware() func(http.Handler) http.Handler {
|
||||
return func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// We can check the cookie directly b/c the filter and controllerRegistry is executed after middleware, so no session
|
||||
// cookie is added by beego.
|
||||
_, err := r.Cookie(config.SessionCookieName)
|
||||
if err == nil {
|
||||
r = r.WithContext(internal.WithCarrySession(r.Context(), true))
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
58
src/server/middleware/session/session_test.go
Normal file
58
src/server/middleware/session/session_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
// 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 session
|
||||
|
||||
import (
|
||||
"github.com/astaxie/beego"
|
||||
beegosession "github.com/astaxie/beego/session"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
carrySession := false
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
carrySession = internal.GetCarrySession(r.Context())
|
||||
})
|
||||
// no session
|
||||
req, err := http.NewRequest("POST", "http://127.0.0.1:8080/api/users", nil)
|
||||
require.Nil(t, err)
|
||||
Middleware()(handler).ServeHTTP(nil, req)
|
||||
assert.False(t, carrySession)
|
||||
|
||||
// contains session
|
||||
beego.BConfig.WebConfig.Session.SessionName = config.SessionCookieName
|
||||
conf := &beegosession.ManagerConfig{
|
||||
CookieName: beego.BConfig.WebConfig.Session.SessionName,
|
||||
Gclifetime: beego.BConfig.WebConfig.Session.SessionGCMaxLifetime,
|
||||
ProviderConfig: filepath.ToSlash(beego.BConfig.WebConfig.Session.SessionProviderConfig),
|
||||
Secure: beego.BConfig.Listen.EnableHTTPS,
|
||||
EnableSetCookie: beego.BConfig.WebConfig.Session.SessionAutoSetCookie,
|
||||
Domain: beego.BConfig.WebConfig.Session.SessionDomain,
|
||||
CookieLifeTime: beego.BConfig.WebConfig.Session.SessionCookieLifeTime,
|
||||
}
|
||||
beego.GlobalSessions, err = beegosession.NewManager("memory", conf)
|
||||
require.Nil(t, err)
|
||||
_, err = beego.GlobalSessions.SessionStart(httptest.NewRecorder(), req)
|
||||
require.Nil(t, err)
|
||||
Middleware()(handler).ServeHTTP(nil, req)
|
||||
assert.True(t, carrySession)
|
||||
}
|
Loading…
Reference in New Issue
Block a user