Merge pull request #11144 from ywk253100/200319_security_middleware

Rewrite the filters with middleware mechinism
This commit is contained in:
Wenkai Yin(尹文开) 2020-03-23 10:12:48 +08:00 committed by GitHub
commit 8688f78cd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1085 additions and 1014 deletions

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -212,17 +212,15 @@ 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 user == nil {
return 0, ErrorUserNotExist
}
if err != nil {
return 0, err
}
if user != nil {
err = OnBoardUser(user)
if err != nil {
return 0, err
}
if user == nil {
return 0, ErrorUserNotExist
}
err = OnBoardUser(user)
if err != nil {
return 0, err
}
return user.UserID, nil
}

View File

@ -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})

View File

@ -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"
)
@ -86,9 +86,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"))

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)))
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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() {

View File

@ -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

View File

@ -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
}

View File

@ -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

View 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
}

View File

@ -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) {

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
})
}
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
})
}
}

View 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)
}