mirror of https://github.com/goharbor/harbor.git
				
				
				
			Provide HTTP authenticator
An HTTP authenticator verifies the credentials by sending a POST request to an HTTP endpoint. After successful authentication he will be onboarded to Harbor's local DB and assigned a role in a project. This commit provides the initial implementation. Currently one limitation is that we don't have clear definition about how we would "search" a user via this HTTP authenticator, a flag for "alway onboard" is provided to skip the search, otherwise, a user has to login first before he can be assigned a role in Harbor. Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
		
							parent
							
								
									6888c3247c
								
							
						
					
					
						commit
						20db0e737b
					
				|  | @ -19,6 +19,7 @@ const ( | |||
| 	DBAuth              = "db_auth" | ||||
| 	LDAPAuth            = "ldap_auth" | ||||
| 	UAAAuth             = "uaa_auth" | ||||
| 	HTTPAuth            = "http_auth" | ||||
| 	ProCrtRestrEveryone = "everyone" | ||||
| 	ProCrtRestrAdmOnly  = "adminonly" | ||||
| 	LDAPScopeBase       = 0 | ||||
|  |  | |||
|  | @ -123,7 +123,7 @@ func Register(name string, h AuthenticateHelper) { | |||
| 		return | ||||
| 	} | ||||
| 	registry[name] = h | ||||
| 	log.Debugf("Registered authencation helper for auth mode: %s", name) | ||||
| 	log.Debugf("Registered authentication helper for auth mode: %s", name) | ||||
| } | ||||
| 
 | ||||
| // Login authenticates user credentials based on setting.
 | ||||
|  |  | |||
|  | @ -0,0 +1,143 @@ | |||
| // Copyright 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 authproxy | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"github.com/goharbor/harbor/src/common" | ||||
| 	"github.com/goharbor/harbor/src/common/dao" | ||||
| 	"github.com/goharbor/harbor/src/common/models" | ||||
| 	"github.com/goharbor/harbor/src/common/utils/log" | ||||
| 	"github.com/goharbor/harbor/src/core/auth" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| // Auth implements HTTP authenticator the required attributes.
 | ||||
| // The attribute Endpoint is the HTTP endpoint to which the POST request should be issued for authentication
 | ||||
| type Auth struct { | ||||
| 	auth.DefaultAuthenticateHelper | ||||
| 	sync.Mutex | ||||
| 	Endpoint       string | ||||
| 	SkipCertVerify bool | ||||
| 	AlwaysOnboard  bool | ||||
| 	client         *http.Client | ||||
| } | ||||
| 
 | ||||
| // Authenticate issues http POST request to Endpoint if it returns 200 the authentication is considered success.
 | ||||
| func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) { | ||||
| 	a.ensure() | ||||
| 	req, err := http.NewRequest(http.MethodPost, a.Endpoint, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to send request, error: %v", err) | ||||
| 	} | ||||
| 	req.SetBasicAuth(m.Principal, m.Password) | ||||
| 	resp, err := a.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	if resp.StatusCode == http.StatusOK { | ||||
| 		return &models.User{Username: m.Principal}, nil | ||||
| 	} else if resp.StatusCode == http.StatusUnauthorized { | ||||
| 		return nil, auth.ErrAuth{} | ||||
| 	} else { | ||||
| 		data, err := ioutil.ReadAll(resp.Body) | ||||
| 		if err != nil { | ||||
| 			log.Warningf("Failed to read response body, error: %v", err) | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("failed to authenticate, status code: %d, text: %s", resp.StatusCode, string(data)) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // OnBoardUser delegates to dao pkg to insert/update data in DB.
 | ||||
| func (a *Auth) OnBoardUser(u *models.User) error { | ||||
| 	return dao.OnBoardUser(u) | ||||
| } | ||||
| 
 | ||||
| // PostAuthenticate generates the user model and on board the user.
 | ||||
| func (a *Auth) PostAuthenticate(u *models.User) error { | ||||
| 	if res, _ := dao.GetUser(*u); res != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if err := a.fillInModel(u); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return a.OnBoardUser(u) | ||||
| } | ||||
| 
 | ||||
| // SearchUser - TODO: Remove this workaround when #6767 is fixed.
 | ||||
| // When the flag is set it always return the default model without searching
 | ||||
| func (a *Auth) SearchUser(username string) (*models.User, error) { | ||||
| 	a.ensure() | ||||
| 	var queryCondition = models.User{ | ||||
| 		Username: username, | ||||
| 	} | ||||
| 	u, err := dao.GetUser(queryCondition) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if a.AlwaysOnboard && u == nil { | ||||
| 		u = &models.User{Username: username} | ||||
| 		if err := a.fillInModel(u); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	return u, nil | ||||
| } | ||||
| 
 | ||||
| func (a *Auth) fillInModel(u *models.User) error { | ||||
| 	if strings.TrimSpace(u.Username) == "" { | ||||
| 		return fmt.Errorf("username cannot be empty") | ||||
| 	} | ||||
| 	u.Realname = u.Username | ||||
| 	u.Password = "1234567ab" | ||||
| 	u.Comment = "By Authproxy" | ||||
| 	if strings.Contains(u.Username, "@") { | ||||
| 		u.Email = u.Username | ||||
| 	} else { | ||||
| 		u.Email = fmt.Sprintf("%s@placeholder.com", u.Username) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (a *Auth) ensure() { | ||||
| 	a.Lock() | ||||
| 	defer a.Unlock() | ||||
| 	if a.Endpoint == "" { | ||||
| 		a.Endpoint = os.Getenv("AUTHPROXY_ENDPOINT") | ||||
| 		a.SkipCertVerify = strings.EqualFold(os.Getenv("AUTHPROXY_SKIP_CERT_VERIFY"), "true") | ||||
| 		a.AlwaysOnboard = strings.EqualFold(os.Getenv("AUTHPROXY_ALWAYS_ONBOARD"), "true") | ||||
| 	} | ||||
| 	if a.client == nil { | ||||
| 		tr := &http.Transport{ | ||||
| 			TLSClientConfig: &tls.Config{ | ||||
| 				InsecureSkipVerify: a.SkipCertVerify, | ||||
| 			}, | ||||
| 		} | ||||
| 		a.client = &http.Client{ | ||||
| 			Transport: tr, | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	auth.Register(common.HTTPAuth, &Auth{}) | ||||
| } | ||||
|  | @ -0,0 +1,144 @@ | |||
| // Copyright 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 authproxy | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/goharbor/harbor/src/common/models" | ||||
| 	"github.com/goharbor/harbor/src/core/auth" | ||||
| 	"github.com/goharbor/harbor/src/core/auth/authproxy/test" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"net/http/httptest" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| var mockSvr *httptest.Server | ||||
| var a *Auth | ||||
| var pwd = "1234567ab" | ||||
| var cmt = "By Authproxy" | ||||
| 
 | ||||
| func TestMain(m *testing.M) { | ||||
| 	mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"}) | ||||
| 	defer mockSvr.Close() | ||||
| 	a = &Auth{ | ||||
| 		Endpoint:       mockSvr.URL + "/test/login", | ||||
| 		SkipCertVerify: true, | ||||
| 	} | ||||
| 	rc := m.Run() | ||||
| 	if rc != 0 { | ||||
| 		os.Exit(rc) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestAuth_Authenticate(t *testing.T) { | ||||
| 	t.Log("auth endpoint: ", a.Endpoint) | ||||
| 	type output struct { | ||||
| 		user models.User | ||||
| 		err  error | ||||
| 	} | ||||
| 	type tc struct { | ||||
| 		input  models.AuthModel | ||||
| 		expect output | ||||
| 	} | ||||
| 	suite := []tc{ | ||||
| 		{ | ||||
| 			input: models.AuthModel{ | ||||
| 				Principal: "jt", Password: "pp"}, | ||||
| 			expect: output{ | ||||
| 				user: models.User{ | ||||
| 					Username: "jt", | ||||
| 				}, | ||||
| 				err: nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: models.AuthModel{ | ||||
| 				Principal: "Admin@vsphere.local", | ||||
| 				Password:  "Admin!23", | ||||
| 			}, | ||||
| 			expect: output{ | ||||
| 				user: models.User{ | ||||
| 					Username: "Admin@vsphere.local", | ||||
| 					// Email:    "Admin@placeholder.com",
 | ||||
| 					// Password: pwd,
 | ||||
| 					// Comment:  fmt.Sprintf(cmtTmpl, path.Join(mockSvr.URL, "/test/login")),
 | ||||
| 				}, | ||||
| 				err: nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: models.AuthModel{ | ||||
| 				Principal: "jt", | ||||
| 				Password:  "ppp", | ||||
| 			}, | ||||
| 			expect: output{ | ||||
| 				err: auth.ErrAuth{}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	assert := assert.New(t) | ||||
| 	for _, c := range suite { | ||||
| 		r, e := a.Authenticate(c.input) | ||||
| 		if c.expect.err == nil { | ||||
| 			assert.Nil(e) | ||||
| 			assert.Equal(c.expect.user, *r) | ||||
| 		} else { | ||||
| 			assert.Nil(r) | ||||
| 			assert.NotNil(e) | ||||
| 			if _, ok := e.(auth.ErrAuth); ok { | ||||
| 				assert.IsType(auth.ErrAuth{}, e) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /* TODO: Enable this case after adminserver refactor is merged. | ||||
| func TestAuth_PostAuthenticate(t *testing.T) { | ||||
| 	type tc struct { | ||||
| 		input  *models.User | ||||
| 		expect models.User | ||||
| 	} | ||||
| 	suite := []tc{ | ||||
| 		{ | ||||
| 			input: &models.User{ | ||||
| 				Username: "jt", | ||||
| 			}, | ||||
| 			expect: models.User{ | ||||
| 				Username: "jt", | ||||
| 				Email:    "jt@placeholder.com", | ||||
| 				Realname: "jt", | ||||
| 				Password: pwd, | ||||
| 				Comment:  fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: &models.User{ | ||||
| 				Username: "Admin@vsphere.local", | ||||
| 			}, | ||||
| 			expect: models.User{ | ||||
| 				Username: "Admin@vsphere.local", | ||||
| 				Email:    "jt@placeholder.com", | ||||
| 				Realname: "Admin@vsphere.local", | ||||
| 				Password: pwd, | ||||
| 				Comment:  fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, c := range suite { | ||||
| 		a.PostAuthenticate(c.input) | ||||
| 		assert.Equal(t, c.expect, *c.input) | ||||
| 	} | ||||
| } | ||||
| */ | ||||
|  | @ -0,0 +1,49 @@ | |||
| // Copyright 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 test | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| ) | ||||
| 
 | ||||
| type authHandler struct { | ||||
| 	m map[string]string | ||||
| } | ||||
| 
 | ||||
| // ServeHTTP handles HTTP requests
 | ||||
| func (ah *authHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||||
| 	if req.Method != http.MethodPost { | ||||
| 		http.Error(rw, "", http.StatusMethodNotAllowed) | ||||
| 	} | ||||
| 	if u, p, ok := req.BasicAuth(); !ok { | ||||
| 		// Simulate a service error
 | ||||
| 		http.Error(rw, "", http.StatusInternalServerError) | ||||
| 	} else if pass, ok := ah.m[u]; !ok || pass != p { | ||||
| 		http.Error(rw, "", http.StatusUnauthorized) | ||||
| 	} else { | ||||
| 		_, e := rw.Write([]byte(`{"session_id": "hgx59wuWI3b0jcbtidv5mU1YCp-DOQ9NKR1iYKACdKCvbVn7"}`)) | ||||
| 		if e != nil { | ||||
| 			panic(e) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewMockServer creates the mock server for testing
 | ||||
| func NewMockServer(creds map[string]string) *httptest.Server { | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.Handle("/test/login", &authHandler{m: creds}) | ||||
| 	return httptest.NewTLSServer(mux) | ||||
| } | ||||
|  | @ -63,7 +63,7 @@ func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) { | |||
| func (u *Auth) OnBoardUser(user *models.User) error { | ||||
| 	user.Username = strings.TrimSpace(user.Username) | ||||
| 	if len(user.Username) == 0 { | ||||
| 		return fmt.Errorf("The Username is empty") | ||||
| 		return fmt.Errorf("the Username is empty") | ||||
| 	} | ||||
| 	if len(user.Password) == 0 { | ||||
| 		user.Password = "1234567ab" | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ import ( | |||
| 	"github.com/goharbor/harbor/src/common/utils" | ||||
| 	"github.com/goharbor/harbor/src/common/utils/log" | ||||
| 	"github.com/goharbor/harbor/src/core/api" | ||||
| 	_ "github.com/goharbor/harbor/src/core/auth/authproxy" | ||||
| 	_ "github.com/goharbor/harbor/src/core/auth/db" | ||||
| 	_ "github.com/goharbor/harbor/src/core/auth/ldap" | ||||
| 	_ "github.com/goharbor/harbor/src/core/auth/uaa" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue