diff --git a/src/jobservice/api/authenticator.go b/src/jobservice/api/authenticator.go new file mode 100644 index 000000000..e1d4cb2dd --- /dev/null +++ b/src/jobservice/api/authenticator.go @@ -0,0 +1,62 @@ +// Copyright 2018 The Harbor Authors. All rights reserved. + +package api + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/vmware/harbor/src/jobservice/config" + "github.com/vmware/harbor/src/jobservice/utils" +) + +const ( + secretPrefix = "Harbor-Secret" + authHeader = "Authorization" +) + +//Authenticator defined behaviors of doing auth checking. +type Authenticator interface { + //Auth incoming request + // + //req *http.Request: the incoming request + // + //Returns: + // nil returned if successfully done + // otherwise an error returned + DoAuth(req *http.Request) error +} + +//SecretAuthenticator implements interface 'Authenticator' based on simple secret. +type SecretAuthenticator struct{} + +//DoAuth implements same method in interface 'Authenticator'. +func (sa *SecretAuthenticator) DoAuth(req *http.Request) error { + if req == nil { + return errors.New("nil request") + } + + h := strings.TrimSpace(req.Header.Get(authHeader)) + if utils.IsEmptyStr(h) { + return fmt.Errorf("header '%s' missing", authHeader) + } + + if !strings.HasPrefix(h, secretPrefix) { + return fmt.Errorf("'%s' should start with '%s' but got '%s' now", authHeader, secretPrefix, h) + } + + secret := strings.TrimSpace(strings.TrimPrefix(h, secretPrefix)) + //incase both two are empty + if utils.IsEmptyStr(secret) { + return errors.New("empty secret is not allowed") + } + + expectedSecret := config.GetUIAuthSecret() + if expectedSecret != secret { + return errors.New("unauthorized") + } + + return nil +} diff --git a/src/jobservice/api/handler_test.go b/src/jobservice/api/handler_test.go index ce6e3a511..c89fa7c74 100644 --- a/src/jobservice/api/handler_test.go +++ b/src/jobservice/api/handler_test.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "math/rand" "net/http" + "os" "strings" "sync" "testing" @@ -18,8 +19,11 @@ import ( "github.com/vmware/harbor/src/jobservice/models" ) +const fakeSecret = "I'mfakesecret" + +var testingAuthProvider = &SecretAuthenticator{} var testingHandler = NewDefaultHandler(&fakeController{}) -var testingRouter = NewBaseRouter(testingHandler) +var testingRouter = NewBaseRouter(testingHandler, testingAuthProvider) var client = &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ @@ -28,7 +32,28 @@ var client = &http.Client{ }, } +func TestUnAuthorizedAccess(t *testing.T) { + exportUISecret("hello") + + server, port, ctx := createServer() + server.Start() + <-time.After(200 * time.Millisecond) + + res, err := getReq(fmt.Sprintf("http://localhost:%d/api/v1/jobs/fake_job", port)) + if e := expectFormatedError(res, err); e != nil { + t.Fatal(e) + } + if strings.Index(err.Error(), "401") == -1 { + t.Fatalf("expect '401' but got none 401 error") + } + + server.Stop() + ctx.WG.Wait() +} + func TestLaunchJobFailed(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -43,6 +68,8 @@ func TestLaunchJobFailed(t *testing.T) { } func TestLaunchJobSucceed(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -64,6 +91,8 @@ func TestLaunchJobSucceed(t *testing.T) { } func TestGetJobFailed(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -78,6 +107,8 @@ func TestGetJobFailed(t *testing.T) { } func TestGetJobSucceed(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -99,6 +130,8 @@ func TestGetJobSucceed(t *testing.T) { } func TestJobActionFailed(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -129,6 +162,8 @@ func TestJobActionFailed(t *testing.T) { } func TestJobActionSucceed(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -165,6 +200,8 @@ func TestJobActionSucceed(t *testing.T) { } func TestCheckStatus(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -191,6 +228,8 @@ func TestCheckStatus(t *testing.T) { } func TestGetJobLog(t *testing.T) { + exportUISecret(fakeSecret) + server, port, ctx := createServer() server.Start() <-time.After(200 * time.Millisecond) @@ -269,6 +308,8 @@ func postReq(url string, data []byte) ([]byte, error) { return nil, err } + req.Header.Set(authHeader, fmt.Sprintf("%s %s", secretPrefix, fakeSecret)) + res, err := client.Do(req) if err != nil { return nil, err @@ -299,6 +340,8 @@ func getReq(url string) ([]byte, error) { return nil, err } + req.Header.Set(authHeader, fmt.Sprintf("%s %s", secretPrefix, fakeSecret)) + res, err := client.Do(req) if err != nil { return nil, err @@ -317,6 +360,10 @@ func getReq(url string) ([]byte, error) { return data, nil } +func exportUISecret(secret string) { + os.Setenv("UI_SECRET", secret) +} + type fakeController struct{} func (fc *fakeController) LaunchJob(req models.JobRequest) (models.JobStats, error) { diff --git a/src/jobservice/api/router.go b/src/jobservice/api/router.go index d24cd575e..b630d3577 100644 --- a/src/jobservice/api/router.go +++ b/src/jobservice/api/router.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/vmware/harbor/src/jobservice/errs" ) const ( @@ -28,13 +29,17 @@ type BaseRouter struct { //Handler used to handle the requests handler Handler + + //Do auth + authenticator Authenticator } //NewBaseRouter is the constructor of BaseRouter. -func NewBaseRouter(handler Handler) Router { +func NewBaseRouter(handler Handler, authenticator Authenticator) Router { br := &BaseRouter{ - router: mux.NewRouter(), - handler: handler, + router: mux.NewRouter(), + handler: handler, + authenticator: authenticator, } //Register routes here @@ -45,6 +50,14 @@ func NewBaseRouter(handler Handler) Router { //ServeHTTP is the implementation of Router interface. func (br *BaseRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { + //Do auth + if err := br.authenticator.DoAuth(req); err != nil { + authErr := errs.UnauthorizedError(err) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(authErr.Error())) + return + } + //Directly pass requests to the server mux. br.router.ServeHTTP(w, req) } diff --git a/src/jobservice/config/config.go b/src/jobservice/config/config.go index 304bccfad..abfcc8d3f 100644 --- a/src/jobservice/config/config.go +++ b/src/jobservice/config/config.go @@ -38,6 +38,9 @@ const ( //JobServicePoolBackendRedis represents redis backend JobServicePoolBackendRedis = "redis" + + //secret of UI + uiAuthSecret = "UI_SECRET" ) //DefaultConfig is the default configuration reference @@ -151,6 +154,11 @@ func GetAuthSecret() string { return utils.ReadEnv(jobServiceAuthSecret) } +//GetUIAuthSecret get the auth secret of UI side +func GetUIAuthSecret() string { + return utils.ReadEnv(uiAuthSecret) +} + //GetAdminServerEndpoint return the admin server endpoint func GetAdminServerEndpoint() string { return DefaultConfig.AdminServer diff --git a/src/jobservice/errs/errors.go b/src/jobservice/errs/errors.go index b7e9715c5..2c58d1c88 100644 --- a/src/jobservice/errs/errors.go +++ b/src/jobservice/errs/errors.go @@ -36,6 +36,8 @@ const ( GetJobLogErrorCode //NoObjectFoundErrorCode is code for the error of no object found NoObjectFoundErrorCode + //UnAuthorizedErrorCode is code for the error of unauthorized accessing + UnAuthorizedErrorCode ) //baseError ... @@ -118,6 +120,11 @@ func GetJobLogError(err error) error { return New(GetJobLogErrorCode, "Failed to get the job log", err.Error()) } +//UnauthorizedError is error for the case of unauthorized accessing +func UnauthorizedError(err error) error { + return New(UnAuthorizedErrorCode, "Unauthorized", err.Error()) +} + //jobStoppedError is designed for the case of stopping job. type jobStoppedError struct { baseError diff --git a/src/jobservice/runtime/bootstrap.go b/src/jobservice/runtime/bootstrap.go index 245036b84..c66240fa8 100644 --- a/src/jobservice/runtime/bootstrap.go +++ b/src/jobservice/runtime/bootstrap.go @@ -125,8 +125,9 @@ func (bs *Bootstrap) LoadAndRun() { //Load and run the API server. func (bs *Bootstrap) loadAndRunAPIServer(ctx *env.Context, cfg *config.Configuration, ctl *core.Controller) *api.Server { //Initialized API server + authProvider := &api.SecretAuthenticator{} handler := api.NewDefaultHandler(ctl) - router := api.NewBaseRouter(handler) + router := api.NewBaseRouter(handler, authProvider) serverConfig := api.ServerConfig{ Protocol: cfg.Protocol, Port: cfg.Port,