diff --git a/make/dev/docker-compose.yml b/make/dev/docker-compose.yml index 6f54d5757..17ac4c552 100644 --- a/make/dev/docker-compose.yml +++ b/make/dev/docker-compose.yml @@ -83,6 +83,7 @@ services: - ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z - /data/secretkey:/etc/ui/key:z - /data/ca_download/:/etc/ui/ca/:z + - /data/service_token:/etc/ui/service_token:z networks: - harbor depends_on: diff --git a/make/docker-compose.tpl b/make/docker-compose.tpl index 22eba44f7..8994ba776 100644 --- a/make/docker-compose.tpl +++ b/make/docker-compose.tpl @@ -77,6 +77,7 @@ services: - ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z - /data/secretkey:/etc/ui/key:z - /data/ca_download/:/etc/ui/ca/:z + - /data/service_token:/etc/ui/service_token:z networks: - harbor depends_on: diff --git a/src/ui/config/config.go b/src/ui/config/config.go index 520fbaec0..f4e7a394e 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -15,8 +15,10 @@ package config import ( + "crypto/tls" "encoding/json" "fmt" + "net/http" "os" "strings" @@ -33,8 +35,9 @@ import ( ) const ( - defaultKeyPath string = "/etc/ui/key" - secretCookieName string = "secret" + defaultKeyPath string = "/etc/ui/key" + defaultTokenFilePath string = "/etc/ui/service_token" + secretCookieName string = "secret" ) var ( @@ -46,6 +49,9 @@ var ( GlobalProjectMgr projectmanager.ProjectManager mg *comcfg.Manager keyProvider comcfg.KeyProvider + // AdmiralClient is initialized only under integration deploy mode + // and can be passed to project manager as a parameter + AdmiralClient *http.Client ) // Init configurations @@ -105,9 +111,19 @@ func initProjectManager() { } // integration with admiral - // TODO create project manager based on pms using service account log.Info("initializing the project manager based on PMS...") - GlobalProjectMgr = pms.NewProjectManager(AdmiralEndpoint(), "") + // TODO read ca/cert file and pass it to the TLS config + AdminserverClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + GlobalProjectMgr = pms.NewProjectManager(AdminserverClient, + AdmiralEndpoint(), &pms.FileTokenReader{ + Path: defaultTokenFilePath, + }) } // Load configurations diff --git a/src/ui/filter/security.go b/src/ui/filter/security.go index 990c7d5b8..6b56ec3bc 100644 --- a/src/ui/filter/security.go +++ b/src/ui/filter/security.go @@ -51,8 +51,8 @@ func Init() { if config.WithAdmiral() { reqCtxModifiers = []ReqCtxModifier{ &secretReqCtxModifier{config.SecretStore}, - &basicAuthReqCtxModifier{}, &tokenReqCtxModifier{}, + &basicAuthReqCtxModifier{}, &unauthorizedReqCtxModifier{}} return } @@ -131,15 +131,14 @@ func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool { if config.WithAdmiral() { // integration with admiral - token, authCtx, err := authcontext.Login(config.AdmiralEndpoint(), username, password) + _, authCtx, err := authcontext.Login(config.AdmiralEndpoint(), username, password) if err != nil { log.Errorf("failed to authenticate %s: %v", username, err) return false } - log.Debug("creating PMS project manager...") - pm = pms.NewProjectManager(config.AdmiralEndpoint(), token) - + log.Debug("using glocal project manager...") + pm = config.GlobalProjectMgr log.Debug("creating admiral security context...") securCtx = admiral.NewSecurityContext(authCtx, pm) } else { @@ -211,7 +210,10 @@ func (t *tokenReqCtxModifier) Modify(ctx *beegoctx.Context) bool { } log.Debug("creating PMS project manager...") - pm := pms.NewProjectManager(config.AdmiralEndpoint(), token) + pm := pms.NewProjectManager(config.AdmiralClient, + config.AdmiralEndpoint(), &pms.RawTokenReader{ + Token: token, + }) log.Debug("creating admiral security context...") securCtx := admiral.NewSecurityContext(authContext, pm) setSecurCtxAndPM(ctx.Request, securCtx, pm) @@ -230,7 +232,8 @@ func (u *unauthorizedReqCtxModifier) Modify(ctx *beegoctx.Context) bool { if config.WithAdmiral() { // integration with admiral log.Debug("creating PMS project manager...") - pm = pms.NewProjectManager(config.AdmiralEndpoint(), "") + pm = pms.NewProjectManager(config.AdmiralClient, + config.AdmiralEndpoint(), nil) log.Debug("creating admiral security context...") securCtx = admiral.NewSecurityContext(nil, pm) } else { diff --git a/src/ui/projectmanager/pms/pm.go b/src/ui/projectmanager/pms/pm.go index 174fb961e..4f8479797 100644 --- a/src/ui/projectmanager/pms/pm.go +++ b/src/ui/projectmanager/pms/pm.go @@ -32,14 +32,12 @@ import ( "github.com/vmware/harbor/src/common/utils/log" ) -var transport = &http.Transport{} - // ProjectManager implements projectmanager.ProjecdtManager interface // base on project management service type ProjectManager struct { - endpoint string - token string - client *http.Client + client *http.Client + endpoint string + tokenReader TokenReader } type user struct { @@ -58,13 +56,12 @@ type project struct { } // NewProjectManager returns an instance of ProjectManager -func NewProjectManager(endpoint, token string) *ProjectManager { +func NewProjectManager(client *http.Client, endpoint string, + tokenReader TokenReader) *ProjectManager { return &ProjectManager{ - endpoint: strings.TrimRight(endpoint, "/"), - token: token, - client: &http.Client{ - Transport: transport, - }, + client: client, + endpoint: strings.TrimRight(endpoint, "/"), + tokenReader: tokenReader, } } @@ -307,7 +304,7 @@ func (p *ProjectManager) GetPublic() ([]*models.Project, error) { // GetByMember ... func (p *ProjectManager) GetByMember(username string) ([]*models.Project, error) { projects := []*models.Project{} - ctx, err := authcontext.GetAuthCtxOfUser(p.endpoint, p.token, username) + ctx, err := authcontext.GetAuthCtxOfUser(p.endpoint, p.getToken(), username) if err != nil { return projects, err } @@ -425,7 +422,7 @@ func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, erro return nil, err } - req.Header.Add("x-xenon-auth-token", p.token) + req.Header.Add("x-xenon-auth-token", p.getToken()) url := req.URL.String() @@ -452,3 +449,16 @@ func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, erro return b, nil } + +func (p *ProjectManager) getToken() string { + if p.tokenReader == nil { + return "" + } + + token, err := p.tokenReader.ReadToken() + if err != nil { + token = "" + log.Errorf("failed to read token: %v", err) + } + return token +} diff --git a/src/ui/projectmanager/pms/pm_test.go b/src/ui/projectmanager/pms/pm_test.go index 17ded36af..aa14d6fde 100644 --- a/src/ui/projectmanager/pms/pm_test.go +++ b/src/ui/projectmanager/pms/pm_test.go @@ -15,6 +15,7 @@ package pms import ( + "net/http" "sort" "testing" @@ -24,8 +25,11 @@ import ( ) var ( - endpoint = "http://127.0.0.1:8282" - token = "" + client = http.DefaultClient + endpoint = "http://127.0.0.1:8282" + tokenReader = &RawTokenReader{ + Token: "", + } ) func TestConvert(t *testing.T) { @@ -177,7 +181,7 @@ func TestParse(t *testing.T) { } func TestGet(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) name := "project_for_test_get" id, err := pm.Create(&models.Project{ Name: name, @@ -211,7 +215,7 @@ func TestGet(t *testing.T) { } func TestIsPublic(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) // invalid input type public, err := pm.IsPublic([]string{}) @@ -259,7 +263,7 @@ func TestIsPublic(t *testing.T) { } func TestExist(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) // invalid input type exist, err := pm.Exist([]string{}) @@ -289,7 +293,7 @@ func TestExist(t *testing.T) { } func TestGetRoles(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) // nil username, nil project roles, err := pm.GetRoles("", nil) @@ -316,7 +320,7 @@ func TestGetRoles(t *testing.T) { } func TestGetPublic(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) projects, err := pm.GetPublic() assert.Nil(t, nil) @@ -350,7 +354,7 @@ func TestGetByMember(t *testing.T) { } func TestCreate(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) name := "project_for_test_create" id, err := pm.Create(&models.Project{ @@ -375,7 +379,7 @@ func TestCreate(t *testing.T) { } func TestDelete(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) // non-exist project err := pm.Delete(int64(0)) @@ -401,13 +405,13 @@ func TestDelete(t *testing.T) { } func TestUpdate(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) err := pm.Update(nil, nil) assert.NotNil(t, err) } func TestGetAll(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) name1 := "project_for_test_get_all_01" id1, err := pm.Create(&models.Project{ @@ -471,7 +475,7 @@ func TestGetAll(t *testing.T) { } func TestGetTotal(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) total1, err := pm.GetTotal(nil) require.Nil(t, err) @@ -489,13 +493,13 @@ func TestGetTotal(t *testing.T) { } func TestGetHasReadPerm(t *testing.T) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) _, err := pm.GetHasReadPerm() assert.NotNil(t, err) } func delete(t *testing.T, id int64) { - pm := NewProjectManager(endpoint, token) + pm := NewProjectManager(client, endpoint, tokenReader) if err := pm.Delete(id); err != nil { t.Logf("failed to delete project %d: %v", id, err) } diff --git a/src/ui/projectmanager/pms/token.go b/src/ui/projectmanager/pms/token.go new file mode 100644 index 000000000..42ddbe1bf --- /dev/null +++ b/src/ui/projectmanager/pms/token.go @@ -0,0 +1,84 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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 pms + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" +) + +const ( + key = "access_token" +) + +// TokenReader is an interface used to wrap the way how to get token +type TokenReader interface { + // ReadToken reads token + ReadToken() (string, error) +} + +// RawTokenReader just returns the token contained by field Token +type RawTokenReader struct { + Token string +} + +// ReadToken ... +func (r *RawTokenReader) ReadToken() (string, error) { + return r.Token, nil +} + +// FileTokenReader reads token from file +type FileTokenReader struct { + Path string +} + +// ReadToken ... +func (f *FileTokenReader) ReadToken() (string, error) { + file, err := os.Open(f.Path) + if err != nil { + return "", err + } + defer file.Close() + + return readToken(file) +} + +func readToken(reader io.Reader) (string, error) { + if reader == nil { + return "", fmt.Errorf("reader is nil") + } + + r := bufio.NewReader(reader) + for { + line, _, err := r.ReadLine() + if err != nil { + if err == io.EOF { + err = fmt.Errorf("%s not found", key) + } + return "", err + } + + strs := strings.SplitN(string(line), "=", 2) + if len(strs) != 2 { + continue + } + if strs[0] == key { + return strs[1], nil + } + } +} diff --git a/src/ui/projectmanager/pms/token_test.go b/src/ui/projectmanager/pms/token_test.go new file mode 100644 index 000000000..04c1ac6e2 --- /dev/null +++ b/src/ui/projectmanager/pms/token_test.go @@ -0,0 +1,98 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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 pms + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRawTokenReader(t *testing.T) { + raw := "token" + reader := &RawTokenReader{ + Token: raw, + } + + token, err := reader.ReadToken() + require.Nil(t, err) + assert.Equal(t, raw, token) +} + +func TestReadToken(t *testing.T) { + // nil reader + _, err := readToken(nil) + assert.NotNil(t, err) + + // empty + reader := bytes.NewReader([]byte{}) + _, err = readToken(reader) + assert.NotNil(t, err) + + // contains no "access_token" + content := "key1=value\nkey2=value2" + reader = bytes.NewReader([]byte(content)) + _, err = readToken(reader) + assert.NotNil(t, err) + + // contains "access_token" but no "=" + content = "access_token value\nkey2=value2" + reader = bytes.NewReader([]byte(content)) + _, err = readToken(reader) + assert.NotNil(t, err) + + // contains "access_token" and "=", but no value + content = "access_token=\nkey2=value2" + reader = bytes.NewReader([]byte(content)) + token, err := readToken(reader) + require.Nil(t, err) + assert.Len(t, token, 0) + + // valid "access_token" + content = "access_token=token\nkey2=value2" + reader = bytes.NewReader([]byte(content)) + token, err = readToken(reader) + require.Nil(t, err) + assert.Equal(t, "token", token) +} + +func TestFileTokenReader(t *testing.T) { + // file not exist + path := "/tmp/not_exist_file" + reader := &FileTokenReader{ + Path: path, + } + + _, err := reader.ReadToken() + assert.NotNil(t, err) + + // file exist + path = "/tmp/exist_file" + err = ioutil.WriteFile(path, []byte("access_token=token"), 0x0666) + require.Nil(t, err) + defer os.Remove(path) + + reader = &FileTokenReader{ + Path: path, + } + + token, err := reader.ReadToken() + require.Nil(t, err) + assert.Equal(t, "token", token) +} diff --git a/src/ui/proxy/interceptor_test.go b/src/ui/proxy/interceptor_test.go index a3157beda..0c84f9ba9 100644 --- a/src/ui/proxy/interceptor_test.go +++ b/src/ui/proxy/interceptor_test.go @@ -112,7 +112,6 @@ func TestEnvPolicyChecker(t *testing.T) { } func TestPMSPolicyChecker(t *testing.T) { - var defaultConfigAdmiral = map[string]interface{}{ common.ExtEndpoint: "https://" + endpoint, common.WithNotary: true, @@ -131,7 +130,8 @@ func TestPMSPolicyChecker(t *testing.T) { panic(err) } - pm := pms.NewProjectManager(admiralEndpoint, token) + pm := pms.NewProjectManager(http.DefaultClient, + admiralEndpoint, nil) name := "project_for_test_get_sev_low" id, err := pm.Create(&models.Project{ Name: name, diff --git a/src/ui/proxy/interceptors.go b/src/ui/proxy/interceptors.go index a31b52e46..af32a834f 100644 --- a/src/ui/proxy/interceptors.go +++ b/src/ui/proxy/interceptors.go @@ -9,7 +9,6 @@ import ( // "github.com/vmware/harbor/src/ui/api" "github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/projectmanager" - "github.com/vmware/harbor/src/ui/projectmanager/pms" "context" "fmt" @@ -102,7 +101,7 @@ func newPMSPolicyChecker(pm projectmanager.ProjectManager) policyChecker { // TODO: Get project manager with PM factory. func getPolicyChecker() policyChecker { if config.WithAdmiral() { - return newPMSPolicyChecker(pms.NewProjectManager(config.AdmiralEndpoint(), "")) + return newPMSPolicyChecker(config.GlobalProjectMgr) } return EnvChecker } diff --git a/src/ui/service/notification.go b/src/ui/service/notification.go index cf1a846b5..6aef9938f 100644 --- a/src/ui/service/notification.go +++ b/src/ui/service/notification.go @@ -27,7 +27,6 @@ import ( "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/ui/api" "github.com/vmware/harbor/src/ui/config" - "github.com/vmware/harbor/src/ui/projectmanager/pms" uiutils "github.com/vmware/harbor/src/ui/utils" ) @@ -161,9 +160,7 @@ func autoScanEnabled(projectName string) bool { return false } if config.WithAdmiral() { - //TODO get a project manager based on service account. - var pm *pms.ProjectManager = pms.NewProjectManager("", "") - p, err := pm.Get(projectName) + p, err := config.GlobalProjectMgr.Get(projectName) if err != nil { log.Warningf("failed to get project, error: %v", err) return false