create different security context according to the rquest

This commit is contained in:
Wenkai Yin 2017-05-04 19:12:16 +08:00
parent b4c172b754
commit f8615e4746
10 changed files with 347 additions and 37 deletions

View File

@ -29,6 +29,9 @@ const (
RoleDeveloper = 3 RoleDeveloper = 3
RoleGuest = 4 RoleGuest = 4
DeployModeStandAlone = "standalone"
DeployModeIntegration = "integration"
ExtEndpoint = "ext_endpoint" ExtEndpoint = "ext_endpoint"
AUTHMode = "auth_mode" AUTHMode = "auth_mode"
DatabaseType = "database_type" DatabaseType = "database_type"

View File

@ -17,17 +17,17 @@ package rbac
import ( import (
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models" "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 // SecurityContext implements security.Context interface based on database
type SecurityContext struct { type SecurityContext struct {
user *models.User user *models.User
pm pm.PM pm projectmanager.ProjectManager
} }
// NewSecurityContext ... // NewSecurityContext ...
func NewSecurityContext(user *models.User, pm pm.PM) *SecurityContext { func NewSecurityContext(user *models.User, pm projectmanager.ProjectManager) *SecurityContext {
return &SecurityContext{ return &SecurityContext{
user: user, user: user,
pm: pm, pm: pm,

View File

@ -24,7 +24,10 @@ import (
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
comcfg "github.com/vmware/harbor/src/common/config" comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models" "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/common/utils/log"
"github.com/vmware/harbor/src/ui/projectmanager"
"github.com/vmware/harbor/src/ui/projectmanager/db"
) )
const ( const (
@ -33,10 +36,15 @@ const (
) )
var ( var (
// SecretStore manages secrets
SecretStore *secret.Store
// AdminserverClient is a client for adminserver // AdminserverClient is a client for adminserver
AdminserverClient client.Client AdminserverClient client.Client
mg *comcfg.Manager // DBProjectManager is the project manager based on database,
keyProvider comcfg.KeyProvider // it is initialized only the deploy mode is standalone
DBProjectManager projectmanager.ProjectManager
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
) )
// Init configurations // Init configurations
@ -62,6 +70,12 @@ func Init() error {
return err return err
} }
// init secret store
initSecretStore()
// init project manager based on database
initDBProjectManager()
return nil return nil
} }
@ -75,6 +89,20 @@ func initKeyProvider() {
keyProvider = comcfg.NewFileKeyProvider(path) 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 // Load configurations
func Load() error { func Load() error {
_, err := mg.Load() _, err := mg.Load()
@ -271,6 +299,7 @@ func UISecret() string {
// JobserviceSecret returns a secret to mark Jobservice when communicate with // JobserviceSecret returns a secret to mark Jobservice when communicate with
// other component // other component
// TODO replace it with method of SecretStore
func JobserviceSecret() string { func JobserviceSecret() string {
return os.Getenv("JOBSERVICE_SECRET") return os.Getenv("JOBSERVICE_SECRET")
} }
@ -303,3 +332,9 @@ func AdmiralEndpoint() string {
func WithAdmiral() bool { func WithAdmiral() bool {
return len(AdmiralEndpoint()) > 0 return len(AdmiralEndpoint()) > 0
} }
// DeployMode returns the deploy mode
// TODO read from adminserver
func DeployMode() string {
return common.DeployModeStandAlone
}

View File

@ -71,6 +71,7 @@ func (cc *CommonController) Login() {
cc.SetSession("userId", user.UserID) cc.SetSession("userId", user.UserID)
cc.SetSession("username", user.Username) cc.SetSession("username", user.Username)
cc.SetSession("isSysAdmin", user.HasAdminRole == 1)
} }
// LogOut Habor UI // LogOut Habor UI

View File

@ -15,22 +15,29 @@
package filter package filter
import ( import (
"net/http"
"strings" "strings"
"github.com/astaxie/beego/context" beegoctx "github.com/astaxie/beego/context"
"github.com/vmware/harbor/src/common/security" "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/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 ( const (
// HarborSecurityContext is the name of security context passed to handlers // HarborSecurityContext is the name of security context passed to handlers
HarborSecurityContext = "harbor_security_context" 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 // SecurityFilter authenticates the request and passes a security context with it
// which can be used to do some authorization // which can be used to do some authorization
func SecurityFilter(ctx *context.Context) { func SecurityFilter(ctx *beegoctx.Context) {
if ctx == nil { if ctx == nil {
return return
} }
@ -40,20 +47,89 @@ func SecurityFilter(ctx *context.Context) {
return return
} }
if !strings.HasPrefix(req.RequestURI, "/api/") && if !strings.HasPrefix(req.URL.RequestURI(), "/api/") &&
!strings.HasPrefix(req.RequestURI, "/service/") { !strings.HasPrefix(req.URL.RequestURI(), "/service/") {
return return
} }
securityCtx, err := createSecurityContext(req) // fill ctx with security context and project manager
if err != nil { fillContext(ctx)
log.Warningf("failed to create security context: %v", err) }
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 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) { func getProjectManager(ctx *beegoctx.Context) projectmanager.ProjectManager {
return nil, nil 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
} }

View File

@ -15,22 +15,73 @@
package filter package filter
import ( import (
"encoding/json"
"log"
"net/http" "net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing" "testing"
"time"
"github.com/astaxie/beego"
"github.com/astaxie/beego/context" "github.com/astaxie/beego/context"
"github.com/astaxie/beego/session"
"github.com/stretchr/testify/assert" "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) { func TestSecurityFilter(t *testing.T) {
// nil request // nil request
ctx := &context.Context{ ctx, err := newContext(nil)
Request: nil, if err != nil {
Input: context.NewInput(), t.Fatalf("failed to crate context: %v", err)
} }
SecurityFilter(ctx) SecurityFilter(ctx)
securityContext := ctx.Input.GetData(HarborSecurityContext) assert.Nil(t, securityContext(ctx))
assert.Nil(t, securityContext) assert.Nil(t, projectManager(ctx))
// the pattern of request does not need security check // the pattern of request does not need security check
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/static/index.html", nil) 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) t.Fatalf("failed to create request: %v", req)
} }
ctx = &context.Context{ ctx, err = newContext(req)
Request: req, if err != nil {
Input: context.NewInput(), t.Fatalf("failed to crate context: %v", err)
} }
SecurityFilter(ctx) SecurityFilter(ctx)
securityContext = ctx.Input.GetData(HarborSecurityContext) assert.Nil(t, securityContext(ctx))
assert.Nil(t, securityContext) 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]
} }

View File

@ -21,11 +21,11 @@ import (
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
) )
// PM implements pm.PM interface based on database // ProjectManager implements pm.PM interface based on database
type PM struct{} type ProjectManager struct{}
// IsPublic returns whether the project is public or not // 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 project *models.Project
var err error var err error
switch projectIDOrName.(type) { 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 // 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{} roles := []int{}
user, err := dao.GetUser(models.User{ user, err := dao.GetUser(models.User{

View File

@ -71,7 +71,7 @@ func TestMain(m *testing.M) {
} }
func TestIsPublic(t *testing.T) { func TestIsPublic(t *testing.T) {
pms := &PM{} pms := &ProjectManager{}
// project name // project name
assert.True(t, pms.IsPublic("library")) assert.True(t, pms.IsPublic("library"))
// project ID // project ID
@ -83,7 +83,7 @@ func TestIsPublic(t *testing.T) {
} }
func TestGetRoles(t *testing.T) { func TestGetRoles(t *testing.T) {
pm := &PM{} pm := &ProjectManager{}
// non exist user // non exist user
assert.Equal(t, []int{}, assert.Equal(t, []int{},

View File

@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 // to projects
type PM interface { type ProjectManager interface {
IsPublic(projectIDOrName interface{}) bool IsPublic(projectIDOrName interface{}) bool
GetRoles(username string, projectIDOrName interface{}) []int GetRoles(username string, projectIDOrName interface{}) []int
} }

View File

@ -22,6 +22,7 @@ import (
) )
// VerifySecret verifies the UI_SECRET cookie in a http request. // VerifySecret verifies the UI_SECRET cookie in a http request.
// TODO remove
func VerifySecret(r *http.Request, expectedSecret string) bool { func VerifySecret(r *http.Request, expectedSecret string) bool {
c, err := r.Cookie("secret") c, err := r.Cookie("secret")
if err != nil { if err != nil {