diff --git a/src/common/const.go b/src/common/const.go index 806e67169..0bc3ae2f4 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -29,6 +29,9 @@ const ( RoleDeveloper = 3 RoleGuest = 4 + DeployModeStandAlone = "standalone" + DeployModeIntegration = "integration" + ExtEndpoint = "ext_endpoint" AUTHMode = "auth_mode" DatabaseType = "database_type" diff --git a/src/common/security/rbac/context.go b/src/common/security/rbac/context.go index eccf02d28..d29d44ec3 100644 --- a/src/common/security/rbac/context.go +++ b/src/common/security/rbac/context.go @@ -17,17 +17,17 @@ package rbac import ( "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/models" - "github.com/vmware/harbor/src/ui/pm" + "github.com/vmware/harbor/src/ui/projectmanager" ) // SecurityContext implements security.Context interface based on database type SecurityContext struct { user *models.User - pm pm.PM + pm projectmanager.ProjectManager } // NewSecurityContext ... -func NewSecurityContext(user *models.User, pm pm.PM) *SecurityContext { +func NewSecurityContext(user *models.User, pm projectmanager.ProjectManager) *SecurityContext { return &SecurityContext{ user: user, pm: pm, diff --git a/src/ui/config/config.go b/src/ui/config/config.go index 9208e953a..bfbf77c77 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -24,7 +24,10 @@ import ( "github.com/vmware/harbor/src/common" comcfg "github.com/vmware/harbor/src/common/config" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/secret" "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/projectmanager" + "github.com/vmware/harbor/src/ui/projectmanager/db" ) const ( @@ -33,10 +36,15 @@ const ( ) var ( + // SecretStore manages secrets + SecretStore *secret.Store // AdminserverClient is a client for adminserver AdminserverClient client.Client - mg *comcfg.Manager - keyProvider comcfg.KeyProvider + // DBProjectManager is the project manager based on database, + // it is initialized only the deploy mode is standalone + DBProjectManager projectmanager.ProjectManager + mg *comcfg.Manager + keyProvider comcfg.KeyProvider ) // Init configurations @@ -62,6 +70,12 @@ func Init() error { return err } + // init secret store + initSecretStore() + + // init project manager based on database + initDBProjectManager() + return nil } @@ -75,6 +89,20 @@ func initKeyProvider() { keyProvider = comcfg.NewFileKeyProvider(path) } +func initSecretStore() { + m := map[string]string{} + m[secret.JobserviceUser] = JobserviceSecret() + SecretStore = secret.NewStore(m) +} + +func initDBProjectManager() { + if len(DeployMode()) == 0 || + DeployMode() == common.DeployModeStandAlone { + log.Info("initializing the project manager based on database...") + DBProjectManager = &db.ProjectManager{} + } +} + // Load configurations func Load() error { _, err := mg.Load() @@ -271,6 +299,7 @@ func UISecret() string { // JobserviceSecret returns a secret to mark Jobservice when communicate with // other component +// TODO replace it with method of SecretStore func JobserviceSecret() string { return os.Getenv("JOBSERVICE_SECRET") } @@ -303,3 +332,9 @@ func AdmiralEndpoint() string { func WithAdmiral() bool { return len(AdmiralEndpoint()) > 0 } + +// DeployMode returns the deploy mode +// TODO read from adminserver +func DeployMode() string { + return common.DeployModeStandAlone +} diff --git a/src/ui/controllers/base.go b/src/ui/controllers/base.go index 8740967e3..b83af84a5 100644 --- a/src/ui/controllers/base.go +++ b/src/ui/controllers/base.go @@ -71,6 +71,7 @@ func (cc *CommonController) Login() { cc.SetSession("userId", user.UserID) cc.SetSession("username", user.Username) + cc.SetSession("isSysAdmin", user.HasAdminRole == 1) } // LogOut Habor UI diff --git a/src/ui/filter/security.go b/src/ui/filter/security.go index c24669232..096c4edeb 100644 --- a/src/ui/filter/security.go +++ b/src/ui/filter/security.go @@ -15,22 +15,29 @@ package filter import ( - "net/http" "strings" - "github.com/astaxie/beego/context" - "github.com/vmware/harbor/src/common/security" + beegoctx "github.com/astaxie/beego/context" + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/security/rbac" + "github.com/vmware/harbor/src/common/security/secret" "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/auth" + "github.com/vmware/harbor/src/ui/config" + "github.com/vmware/harbor/src/ui/projectmanager" ) const ( // HarborSecurityContext is the name of security context passed to handlers HarborSecurityContext = "harbor_security_context" + // HarborProjectManager is the name of project manager passed to handlers + HarborProjectManager = "harbor_project_manager" ) // SecurityFilter authenticates the request and passes a security context with it // which can be used to do some authorization -func SecurityFilter(ctx *context.Context) { +func SecurityFilter(ctx *beegoctx.Context) { if ctx == nil { return } @@ -40,20 +47,89 @@ func SecurityFilter(ctx *context.Context) { return } - if !strings.HasPrefix(req.RequestURI, "/api/") && - !strings.HasPrefix(req.RequestURI, "/service/") { + if !strings.HasPrefix(req.URL.RequestURI(), "/api/") && + !strings.HasPrefix(req.URL.RequestURI(), "/service/") { return } - securityCtx, err := createSecurityContext(req) - if err != nil { - log.Warningf("failed to create security context: %v", err) + // fill ctx with security context and project manager + fillContext(ctx) +} + +func fillContext(ctx *beegoctx.Context) { + // secret + scrt := ctx.GetCookie("secret") + if len(scrt) != 0 { + ctx.Input.SetData(HarborProjectManager, + getProjectManager(ctx)) + + log.Info("creating a secret security context...") + ctx.Input.SetData(HarborSecurityContext, + secret.NewSecurityContext(scrt, config.SecretStore)) + return } - ctx.Input.SetData(HarborSecurityContext, securityCtx) + var user *models.User + var err error + + // basic auth + username, password, ok := ctx.Request.BasicAuth() + if ok { + // TODO the return data contains other params when integrated + // with vic + user, err = auth.Login(models.AuthModel{ + Principal: username, + Password: password, + }) + if err != nil { + log.Errorf("failed to authenticate %s: %v", username, err) + } + if user != nil { + log.Info("got user information via basic auth") + } + } + + // session + if user == nil { + username := ctx.Input.Session("username") + isSysAdmin := ctx.Input.Session("isSysAdmin") + if username != nil { + user = &models.User{ + Username: username.(string), + } + + if isSysAdmin != nil && isSysAdmin.(bool) { + user.HasAdminRole = 1 + } + log.Info("got user information from session") + } + + // TODO maybe need to get token from session + } + + if user == nil { + log.Info("user information is nil") + } + + pm := getProjectManager(ctx) + ctx.Input.SetData(HarborProjectManager, pm) + + log.Info("creating a rbac security context...") + ctx.Input.SetData(HarborSecurityContext, + rbac.NewSecurityContext(user, pm)) + + return } -func createSecurityContext(req *http.Request) (security.Context, error) { - return nil, nil +func getProjectManager(ctx *beegoctx.Context) projectmanager.ProjectManager { + if len(config.DeployMode()) == 0 || + config.DeployMode() == common.DeployModeStandAlone { + log.Info("filling a project manager based on database...") + return config.DBProjectManager + } + + // TODO create project manager based on pms + log.Info("filling a project manager based on pms...") + return nil } diff --git a/src/ui/filter/security_test.go b/src/ui/filter/security_test.go index 5365c9382..4afd8b42f 100644 --- a/src/ui/filter/security_test.go +++ b/src/ui/filter/security_test.go @@ -15,22 +15,73 @@ package filter import ( + "encoding/json" + "log" "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" "testing" + "time" + "github.com/astaxie/beego" "github.com/astaxie/beego/context" + "github.com/astaxie/beego/session" "github.com/stretchr/testify/assert" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/security" + "github.com/vmware/harbor/src/common/security/rbac" + "github.com/vmware/harbor/src/common/security/secret" + _ "github.com/vmware/harbor/src/ui/auth/db" + _ "github.com/vmware/harbor/src/ui/auth/ldap" + "github.com/vmware/harbor/src/ui/config" ) +func TestMain(m *testing.M) { + // initialize beego session manager + conf := map[string]interface{}{ + "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, + } + confBytes, err := json.Marshal(conf) + if err != nil { + log.Fatalf("failed to marshal session conf: %v", err) + } + + beego.GlobalSessions, err = session.NewManager("memory", string(confBytes)) + if err != nil { + log.Fatalf("failed to create session manager: %v", err) + } + + if err := config.Init(); err != nil { + log.Fatalf("failed to initialize configurations: %v", err) + } + database, err := config.Database() + if err != nil { + log.Fatalf("failed to get database configurations: %v", err) + } + if err = dao.InitDatabase(database); err != nil { + log.Fatalf("failed to initialize database: %v", err) + } + + os.Exit(m.Run()) +} + func TestSecurityFilter(t *testing.T) { // nil request - ctx := &context.Context{ - Request: nil, - Input: context.NewInput(), + ctx, err := newContext(nil) + if err != nil { + t.Fatalf("failed to crate context: %v", err) } SecurityFilter(ctx) - securityContext := ctx.Input.GetData(HarborSecurityContext) - assert.Nil(t, securityContext) + assert.Nil(t, securityContext(ctx)) + assert.Nil(t, projectManager(ctx)) // the pattern of request does not need security check req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/static/index.html", nil) @@ -38,13 +89,156 @@ func TestSecurityFilter(t *testing.T) { t.Fatalf("failed to create request: %v", req) } - ctx = &context.Context{ - Request: req, - Input: context.NewInput(), + ctx, err = newContext(req) + if err != nil { + t.Fatalf("failed to crate context: %v", err) } SecurityFilter(ctx) - securityContext = ctx.Input.GetData(HarborSecurityContext) - assert.Nil(t, securityContext) + assert.Nil(t, securityContext(ctx)) + assert.Nil(t, projectManager(ctx)) - //TODO add a case to test normal process + // 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)) + assert.NotNil(t, projectManager(ctx)) +} + +func TestFillContext(t *testing.T) { + // secret + 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.AddCookie(&http.Cookie{ + Name: "secret", + Value: "secret", + }) + + ctx, err := newContext(req) + if err != nil { + t.Fatalf("failed to crate context: %v", err) + } + + fillContext(ctx) + assert.IsType(t, &secret.SecurityContext{}, + securityContext(ctx)) + assert.NotNil(t, projectManager(ctx)) + + // session + 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("username", "admin"); err != nil { + t.Fatalf("failed to set session: %v", err) + } + if err = store.Set("isSysAdmin", true); 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()) + + ctx, err = newContext(req) + if err != nil { + t.Fatalf("failed to crate context: %v", err) + } + fillContext(ctx) + sc := securityContext(ctx) + assert.IsType(t, &rbac.SecurityContext{}, sc) + s := sc.(security.Context) + assert.Equal(t, "admin", s.GetUsername()) + assert.True(t, s.IsSysAdmin()) + assert.NotNil(t, projectManager(ctx)) + + // basic auth + 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) + } + fillContext(ctx) + sc = securityContext(ctx) + assert.IsType(t, &rbac.SecurityContext{}, sc) + s = sc.(security.Context) + assert.Equal(t, "admin", s.GetUsername()) + assert.NotNil(t, projectManager(ctx)) + + // no credential + 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) + } + fillContext(ctx) + sc = securityContext(ctx) + assert.IsType(t, &rbac.SecurityContext{}, sc) + s = sc.(security.Context) + assert.False(t, s.IsAuthenticated()) + assert.NotNil(t, projectManager(ctx)) +} + +func newContext(req *http.Request) (*context.Context, error) { + var err error + ctx := context.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 *context.Context) interface{} { + return ctx.Input.Data()[HarborSecurityContext] +} + +func projectManager(ctx *context.Context) interface{} { + return ctx.Input.Data()[HarborProjectManager] } diff --git a/src/ui/pm/db/pm.go b/src/ui/projectmanager/db/pm.go similarity index 91% rename from src/ui/pm/db/pm.go rename to src/ui/projectmanager/db/pm.go index cabe127ff..e2cf75da0 100644 --- a/src/ui/pm/db/pm.go +++ b/src/ui/projectmanager/db/pm.go @@ -21,11 +21,11 @@ import ( "github.com/vmware/harbor/src/common/utils/log" ) -// PM implements pm.PM interface based on database -type PM struct{} +// ProjectManager implements pm.PM interface based on database +type ProjectManager struct{} // IsPublic returns whether the project is public or not -func (p *PM) IsPublic(projectIDOrName interface{}) bool { +func (p *ProjectManager) IsPublic(projectIDOrName interface{}) bool { var project *models.Project var err error switch projectIDOrName.(type) { @@ -53,7 +53,7 @@ func (p *PM) IsPublic(projectIDOrName interface{}) bool { } // GetRoles return a role list which contains the user's roles to the project -func (p *PM) GetRoles(username string, projectIDOrName interface{}) []int { +func (p *ProjectManager) GetRoles(username string, projectIDOrName interface{}) []int { roles := []int{} user, err := dao.GetUser(models.User{ diff --git a/src/ui/pm/db/pm_test.go b/src/ui/projectmanager/db/pm_test.go similarity index 98% rename from src/ui/pm/db/pm_test.go rename to src/ui/projectmanager/db/pm_test.go index a106686c9..f1fc7e5f9 100644 --- a/src/ui/pm/db/pm_test.go +++ b/src/ui/projectmanager/db/pm_test.go @@ -71,7 +71,7 @@ func TestMain(m *testing.M) { } func TestIsPublic(t *testing.T) { - pms := &PM{} + pms := &ProjectManager{} // project name assert.True(t, pms.IsPublic("library")) // project ID @@ -83,7 +83,7 @@ func TestIsPublic(t *testing.T) { } func TestGetRoles(t *testing.T) { - pm := &PM{} + pm := &ProjectManager{} // non exist user assert.Equal(t, []int{}, diff --git a/src/ui/pm/pm.go b/src/ui/projectmanager/pm.go similarity index 84% rename from src/ui/pm/pm.go rename to src/ui/projectmanager/pm.go index 8a42fe9f0..3fae44fba 100644 --- a/src/ui/pm/pm.go +++ b/src/ui/projectmanager/pm.go @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pm +package projectmanager -// PM is the project mamager which abstracts the operations related +// ProjectManager is the project mamager which abstracts the operations related // to projects -type PM interface { +type ProjectManager interface { IsPublic(projectIDOrName interface{}) bool GetRoles(username string, projectIDOrName interface{}) []int } diff --git a/src/ui/service/utils/utils.go b/src/ui/service/utils/utils.go index 5d52fed29..7fdebf4c8 100644 --- a/src/ui/service/utils/utils.go +++ b/src/ui/service/utils/utils.go @@ -22,6 +22,7 @@ import ( ) // VerifySecret verifies the UI_SECRET cookie in a http request. +// TODO remove func VerifySecret(r *http.Request, expectedSecret string) bool { c, err := r.Cookie("secret") if err != nil {