// 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" "regexp" beegoctx "github.com/astaxie/beego/context" "github.com/docker/distribution/reference" "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" secstore "github.com/goharbor/harbor/src/common/secret" "github.com/goharbor/harbor/src/common/security" admr "github.com/goharbor/harbor/src/common/security/admiral" "github.com/goharbor/harbor/src/common/security/admiral/authcontext" "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/token" "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/core/promgr" "github.com/goharbor/harbor/src/core/promgr/pmsdriver/admiral" "strings" ) // ContextValueKey for content value type ContextValueKey string type pathMethod struct { path string method string } const ( // SecurCtxKey is context value key for security context SecurCtxKey ContextValueKey = "harbor_security_context" // PmKey is context value key for the project manager PmKey ContextValueKey = "harbor_project_manager" ) var ( reqCtxModifiers []ReqCtxModifier // basic auth request context modifier only takes effect on the patterns // in the slice basicAuthReqPatterns = []*pathMethod{ // create project { path: "/api/projects", method: http.MethodPost, }, // token service { path: "/service/token", method: http.MethodGet, }, // delete repository { path: "/api/repositories/" + reference.NameRegexp.String(), method: http.MethodDelete, }, // delete tag { path: "/api/repositories/" + reference.NameRegexp.String() + "/tags/" + reference.TagRegexp.String(), method: http.MethodDelete, }, } ) // Init ReqCtxMofiers list func Init() { // integration with admiral if config.WithAdmiral() { reqCtxModifiers = []ReqCtxModifier{ &secretReqCtxModifier{config.SecretStore}, &tokenReqCtxModifier{}, &basicAuthReqCtxModifier{}, &unauthorizedReqCtxModifier{}} return } // standalone reqCtxModifiers = []ReqCtxModifier{ &secretReqCtxModifier{config.SecretStore}, &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 } 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("using global project manager") pm := config.GlobalProjectMgr log.Debug("creating a secret security context...") securCtx := secret.NewSecurityContext(scrt, s.store) setSecurCtxAndPM(ctx.Request, securCtx, pm) return true } type robotAuthReqCtxModifier struct{} func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool { robotName, robotTk, ok := ctx.Request.BasicAuth() if !ok { return false } log.Debug("got robot information via token auth") if !strings.HasPrefix(robotName, common.RobotPrefix) { return false } rClaims := &token.RobotClaims{} htk := &token.HToken{} htk, err := token.ParseWithClaims(robotTk, rClaims) if err != nil { log.Errorf("failed to decrypt robot token, %v", err) return false } log.Infof(fmt.Sprintf("got robot token header, %v", htk.Header)) // Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable. robot, err := dao.GetRobotByID(htk.Claims.(*token.RobotClaims).TokenID) if err != nil { log.Errorf("failed to get robot %s: %v", robotName, err) return false } if robot == nil { log.Error("the token is not valid.") 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, htk.Claims.(*token.RobotClaims).Policy) setSecurCtxAndPM(ctx.Request, securCtx, pm) return 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") // integration with admiral if config.WithAdmiral() { // Can't get a token from Admiral's login API, we can only // create a project manager with the token of the solution user. // That way may cause some wrong permission promotion in some API // calls, so we just handle the requests which are necessary match := false var err error path := ctx.Request.URL.Path for _, pattern := range basicAuthReqPatterns { match, err = regexp.MatchString(pattern.path, path) if err != nil { log.Errorf("failed to match %s with pattern %s", path, pattern) continue } if match { break } } if !match { log.Debugf("basic auth is not supported for request %s %s, skip", ctx.Request.Method, ctx.Request.URL.Path) return false } token, err := config.TokenReader.ReadToken() if err != nil { log.Errorf("failed to read solution user token: %v", err) return false } authCtx, err := authcontext.Login(config.AdmiralClient, config.AdmiralEndpoint(), username, password, token) if err != nil { log.Errorf("failed to authenticate %s: %v", username, err) return false } log.Debug("using global project manager...") pm := config.GlobalProjectMgr log.Debug("creating admiral security context...") securCtx := admr.NewSecurityContext(authCtx, pm) setSecurCtxAndPM(ctx.Request, securCtx, pm) return true } // standalone 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 } log.Debug("using local database project manager") pm := config.GlobalProjectMgr log.Debug("creating local database security context...") securCtx := local.NewSecurityContext(user, pm) setSecurCtxAndPM(ctx.Request, securCtx, pm) return true } type sessionReqCtxModifier struct{} func (s *sessionReqCtxModifier) Modify(ctx *beegoctx.Context) bool { var user models.User 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 } log.Debugf("Getting user %+v", user) log.Debug("using local database project manager") pm := config.GlobalProjectMgr log.Debug("creating local database security context...") securCtx := local.NewSecurityContext(&user, pm) setSecurCtxAndPM(ctx.Request, securCtx, pm) return true } type tokenReqCtxModifier struct{} func (t *tokenReqCtxModifier) Modify(ctx *beegoctx.Context) bool { token := ctx.Request.Header.Get(authcontext.AuthTokenHeader) if len(token) == 0 { return false } log.Debug("got token from request") authContext, err := authcontext.GetAuthCtx(config.AdmiralClient, config.AdmiralEndpoint(), token) if err != nil { log.Errorf("failed to get auth context: %v", err) return false } log.Debug("creating PMS project manager...") driver := admiral.NewDriver(config.AdmiralClient, config.AdmiralEndpoint(), &admiral.RawTokenReader{ Token: token, }) pm := promgr.NewDefaultProjectManager(driver, false) log.Debug("creating admiral security context...") securCtx := admr.NewSecurityContext(authContext, pm) setSecurCtxAndPM(ctx.Request, securCtx, pm) 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") var securCtx security.Context var pm promgr.ProjectManager if config.WithAdmiral() { // integration with admiral log.Debug("creating PMS project manager...") driver := admiral.NewDriver(config.AdmiralClient, config.AdmiralEndpoint(), nil) pm = promgr.NewDefaultProjectManager(driver, false) log.Debug("creating admiral security context...") securCtx = admr.NewSecurityContext(nil, pm) } else { // standalone log.Debug("using local database project manager") pm = config.GlobalProjectMgr log.Debug("creating local database security context...") securCtx = local.NewSecurityContext(nil, pm) } setSecurCtxAndPM(ctx.Request, securCtx, pm) return true } func setSecurCtxAndPM(req *http.Request, ctx security.Context, pm promgr.ProjectManager) { addToReqContext(req, SecurCtxKey, ctx) addToReqContext(req, PmKey, pm) } func addToReqContext(req *http.Request, key, value interface{}) { *req = *(req.WithContext(context.WithValue(req.Context(), key, value))) } // GetSecurityContext tries to get security context from request and returns it func GetSecurityContext(req *http.Request) (security.Context, error) { if req == nil { return nil, fmt.Errorf("request is nil") } ctx := req.Context().Value(SecurCtxKey) if ctx == nil { return nil, fmt.Errorf("the security context got from request is nil") } c, ok := ctx.(security.Context) if !ok { return nil, fmt.Errorf("the variable got from request is not security context type") } return c, nil } // GetProjectManager tries to get project manager from request and returns it func GetProjectManager(req *http.Request) (promgr.ProjectManager, error) { if req == nil { return nil, fmt.Errorf("request is nil") } pm := req.Context().Value(PmKey) if pm == nil { return nil, fmt.Errorf("the project manager got from request is nil") } p, ok := pm.(promgr.ProjectManager) if !ok { return nil, fmt.Errorf("the variable got from request is not project manager type") } return p, nil }