Merge pull request #10136 from reasonerjt/rm-authproxy-case-sensitive

Get rid of case-sensitivity in authproxy setting
This commit is contained in:
Wang Yan 2019-12-05 14:26:18 +08:00 committed by GitHub
commit 9016c427b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 80 additions and 59 deletions

View File

@ -139,7 +139,6 @@ var (
{Name: common.HTTPAuthProxyVerifyCert, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "true", ItemType: &BoolType{}}, {Name: common.HTTPAuthProxyVerifyCert, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "true", ItemType: &BoolType{}},
{Name: common.HTTPAuthProxySkipSearch, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}}, {Name: common.HTTPAuthProxySkipSearch, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},
{Name: common.HTTPAuthProxyServerCertificate, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}}, {Name: common.HTTPAuthProxyServerCertificate, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{Name: common.HTTPAuthProxyCaseSensitive, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "true", ItemType: &BoolType{}},
{Name: common.OIDCName, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}}, {Name: common.OIDCName, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
{Name: common.OIDCEndpoint, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}}, {Name: common.OIDCEndpoint, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},

View File

@ -104,7 +104,6 @@ const (
HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint" HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint"
HTTPAuthProxyVerifyCert = "http_authproxy_verify_cert" HTTPAuthProxyVerifyCert = "http_authproxy_verify_cert"
HTTPAuthProxySkipSearch = "http_authproxy_skip_search" HTTPAuthProxySkipSearch = "http_authproxy_skip_search"
HTTPAuthProxyCaseSensitive = "http_authproxy_case_sensitive"
HTTPAuthProxyServerCertificate = "http_authproxy_server_certificate" HTTPAuthProxyServerCertificate = "http_authproxy_server_certificate"
OIDCName = "oidc_name" OIDCName = "oidc_name"
OIDCEndpoint = "oidc_endpoint" OIDCEndpoint = "oidc_endpoint"

View File

@ -74,7 +74,6 @@ type HTTPAuthProxy struct {
VerifyCert bool `json:"verify_cert"` VerifyCert bool `json:"verify_cert"`
SkipSearch bool `json:"skip_search"` SkipSearch bool `json:"skip_search"`
ServerCertificate string `json:"server_certificate"` ServerCertificate string `json:"server_certificate"`
CaseSensitive bool `json:"case_sensitive"`
} }
// OIDCSetting wraps the settings for OIDC auth endpoint // OIDCSetting wraps the settings for OIDC auth endpoint

View File

@ -126,7 +126,7 @@ func (c *ConfigAPI) validateCfg(cfgs map[string]interface{}) (bool, error) {
return true, err return true, err
} }
if !flag { if !flag {
if failedKeys := checkUnmodifiable(c.cfgManager, cfgs, common.AUTHMode, common.HTTPAuthProxyCaseSensitive); len(failedKeys) > 0 { if failedKeys := checkUnmodifiable(c.cfgManager, cfgs, common.AUTHMode); len(failedKeys) > 0 {
return false, fmt.Errorf("the keys %v can not be modified as new users have been inserted into database", failedKeys) return false, fmt.Errorf("the keys %v can not be modified as new users have been inserted into database", failedKeys)
} }
} }

View File

@ -53,10 +53,6 @@ type Auth struct {
Endpoint string Endpoint string
TokenReviewEndpoint string TokenReviewEndpoint string
SkipSearch bool SkipSearch bool
// When this attribute is set to false, the name of user/group will be converted to lower-case when onboarded to Harbor, so
// as long as the authentication is successful there's no difference in terms of upper or lower case that is used.
// It will be mapped to one entry in Harbor's User/Group table.
CaseSensitive bool
settingTimeStamp time.Time settingTimeStamp time.Time
client *http.Client client *http.Client
} }
@ -83,26 +79,25 @@ func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
name := a.normalizeName(m.Principal)
user := &models.User{Username: name}
data, err := ioutil.ReadAll(resp.Body) data, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Warningf("Failed to read response body, error: %v", err) log.Warningf("Failed to read response body, error: %v", err)
return nil, auth.ErrAuth{} return nil, auth.ErrAuth{}
} }
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
s := session{} s := session{}
err = json.Unmarshal(data, &s) err = json.Unmarshal(data, &s)
if err != nil { if err != nil {
return nil, auth.NewErrAuth(fmt.Sprintf("failed to read session %v", err)) return nil, auth.NewErrAuth(fmt.Sprintf("failed to read session %v", err))
} }
if err := a.tokenReview(s.SessionID, user); err != nil { user, err := a.tokenReview(s.SessionID)
return nil, auth.NewErrAuth(err.Error()) if err != nil {
return nil, auth.NewErrAuth(fmt.Sprintf("failed to do token review, error: %v", err))
} }
return user, nil return user, nil
} else if resp.StatusCode == http.StatusUnauthorized { } else if resp.StatusCode == http.StatusUnauthorized {
return nil, auth.ErrAuth{} return nil, auth.NewErrAuth(string(data))
} else { } else {
data, err := ioutil.ReadAll(resp.Body) data, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
@ -112,26 +107,24 @@ func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
} }
} }
func (a *Auth) tokenReview(sessionID string, user *models.User) error { func (a *Auth) tokenReview(sessionID string) (*models.User, error) {
httpAuthProxySetting, err := config.HTTPAuthProxySetting() httpAuthProxySetting, err := config.HTTPAuthProxySetting()
if err != nil { if err != nil {
return err return nil, err
} }
reviewStatus, err := authproxy.TokenReview(sessionID, httpAuthProxySetting) reviewStatus, err := authproxy.TokenReview(sessionID, httpAuthProxySetting)
if err != nil { if err != nil {
return err return nil, err
} }
u2, err := authproxy.UserFromReviewStatus(reviewStatus) u, err := authproxy.UserFromReviewStatus(reviewStatus)
if err != nil { if err != nil {
return err return nil, err
} }
user.GroupIDs = u2.GroupIDs return u, nil
return nil
} }
// OnBoardUser delegates to dao pkg to insert/update data in DB. // OnBoardUser delegates to dao pkg to insert/update data in DB.
func (a *Auth) OnBoardUser(u *models.User) error { func (a *Auth) OnBoardUser(u *models.User) error {
u.Username = a.normalizeName(u.Username)
return dao.OnBoardUser(u) return dao.OnBoardUser(u)
} }
@ -154,7 +147,6 @@ func (a *Auth) SearchUser(username string) (*models.User, error) {
if err != nil { if err != nil {
log.Warningf("Failed to refresh configuration for HTTP Auth Proxy Authenticator, error: %v, the default settings will be used", err) log.Warningf("Failed to refresh configuration for HTTP Auth Proxy Authenticator, error: %v, the default settings will be used", err)
} }
username = a.normalizeName(username)
var u *models.User var u *models.User
if a.SkipSearch { if a.SkipSearch {
u = &models.User{Username: username} u = &models.User{Username: username}
@ -171,7 +163,6 @@ func (a *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) {
if err != nil { if err != nil {
log.Warningf("Failed to refresh configuration for HTTP Auth Proxy Authenticator, error: %v, the default settings will be used", err) log.Warningf("Failed to refresh configuration for HTTP Auth Proxy Authenticator, error: %v, the default settings will be used", err)
} }
groupKey = a.normalizeName(groupKey)
var ug *models.UserGroup var ug *models.UserGroup
if a.SkipSearch { if a.SkipSearch {
ug = &models.UserGroup{ ug = &models.UserGroup{
@ -190,7 +181,6 @@ func (a *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
return errors.New("Should provide a group name") return errors.New("Should provide a group name")
} }
u.GroupType = common.HTTPGroupType u.GroupType = common.HTTPGroupType
u.GroupName = a.normalizeName(u.GroupName)
err := group.OnBoardUserGroup(u) err := group.OnBoardUserGroup(u)
if err != nil { if err != nil {
return err return err
@ -248,13 +238,6 @@ func getTLSConfig(setting *models.HTTPAuthProxy) (*tls.Config, error) {
return &tls.Config{InsecureSkipVerify: !setting.VerifyCert}, nil return &tls.Config{InsecureSkipVerify: !setting.VerifyCert}, nil
} }
func (a *Auth) normalizeName(n string) string {
if !a.CaseSensitive {
return strings.ToLower(n)
}
return n
}
func init() { func init() {
auth.Register(common.HTTPAuth, &Auth{}) auth.Register(common.HTTPAuth, &Auth{})
} }

View File

@ -44,13 +44,11 @@ func TestMain(m *testing.M) {
a = &Auth{ a = &Auth{
Endpoint: mockSvr.URL + "/test/login", Endpoint: mockSvr.URL + "/test/login",
TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview", TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview",
CaseSensitive: false,
} }
cfgMap := cut.GetUnitTestConfig() cfgMap := cut.GetUnitTestConfig()
conf := map[string]interface{}{ conf := map[string]interface{}{
common.HTTPAuthProxyEndpoint: a.Endpoint, common.HTTPAuthProxyEndpoint: a.Endpoint,
common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint, common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint,
common.HTTPAuthProxyCaseSensitive: a.CaseSensitive,
common.HTTPAuthProxyVerifyCert: false, common.HTTPAuthProxyVerifyCert: false,
common.PostGreSQLSSLMode: cfgMap[common.PostGreSQLSSLMode], common.PostGreSQLSSLMode: cfgMap[common.PostGreSQLSSLMode],
common.PostGreSQLUsername: cfgMap[common.PostGreSQLUsername], common.PostGreSQLUsername: cfgMap[common.PostGreSQLUsername],
@ -200,7 +198,7 @@ func TestAuth_OnBoardGroup(t *testing.T) {
assert.True(t, input.ID > 0, "The OnBoardGroup should have a valid group ID") assert.True(t, input.ID > 0, "The OnBoardGroup should have a valid group ID")
g, er := group.GetUserGroup(input.ID) g, er := group.GetUserGroup(input.ID)
assert.Nil(t, er) assert.Nil(t, er)
assert.Equal(t, "onboardtest", g.GroupName) assert.Equal(t, "OnBoardTest", g.GroupName)
emptyGroup := &models.UserGroup{} emptyGroup := &models.UserGroup{}
err := a.OnBoardGroup(emptyGroup, "") err := a.OnBoardGroup(emptyGroup, "")
@ -226,7 +224,6 @@ func TestGetTLSConfig(t *testing.T) {
VerifyCert: false, VerifyCert: false,
SkipSearch: false, SkipSearch: false,
ServerCertificate: "", ServerCertificate: "",
CaseSensitive: false,
}, },
expect: result{ expect: result{
hasError: false, hasError: false,
@ -241,7 +238,6 @@ func TestGetTLSConfig(t *testing.T) {
VerifyCert: false, VerifyCert: false,
SkipSearch: false, SkipSearch: false,
ServerCertificate: "This does not look like a cert", ServerCertificate: "This does not look like a cert",
CaseSensitive: false,
}, },
expect: result{ expect: result{
hasError: false, hasError: false,
@ -256,7 +252,6 @@ func TestGetTLSConfig(t *testing.T) {
VerifyCert: true, VerifyCert: true,
SkipSearch: false, SkipSearch: false,
ServerCertificate: "This does not look like a cert", ServerCertificate: "This does not look like a cert",
CaseSensitive: false,
}, },
expect: result{ expect: result{
hasError: true, hasError: true,
@ -269,7 +264,6 @@ func TestGetTLSConfig(t *testing.T) {
VerifyCert: true, VerifyCert: true,
SkipSearch: false, SkipSearch: false,
ServerCertificate: "", ServerCertificate: "",
CaseSensitive: false,
}, },
expect: result{ expect: result{
hasError: false, hasError: false,
@ -315,7 +309,6 @@ pkgODrJUf0p5dhcnLyA2nZolRV1rtwlgJstnEV4JpG1MwtmAZYZUilLvnfpVxTtA
y1bQusZMygQezfCuEzsewF+OpANFovCTUEs6s5vyoVNP8lk= y1bQusZMygQezfCuEzsewF+OpANFovCTUEs6s5vyoVNP8lk=
-----END CERTIFICATE----- -----END CERTIFICATE-----
`, `,
CaseSensitive: false,
}, },
expect: result{ expect: result{
hasError: false, hasError: false,

View File

@ -15,14 +15,30 @@
package test package test
import ( import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common/utils"
"io/ioutil"
"k8s.io/api/authentication/v1beta1"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
) )
type userEntry struct {
username string
password string
sessionID string
reviewStatus string
}
type authHandler struct { type authHandler struct {
entries []userEntry
m map[string]string m map[string]string
} }
var reviewStatusTpl = `{"apiVersion": "authentication.k8s.io/v1beta1", "kind": "TokenReview", "status": {"authenticated": true, "user": {"username": "%s", "groups": ["vsphere.local\\users", "vsphere.local\\administrators", "vsphere.local\\caadmins", "vsphere.local\\systemconfiguration.bashshelladministrators", "vsphere.local\\systemconfiguration.administrators", "vsphere.local\\licenseservice.administrators", "vsphere.local\\everyone"], "extra": {"method": ["basic"]}}}}`
// ServeHTTP handles HTTP requests // ServeHTTP handles HTTP requests
func (ah *authHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (ah *authHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost { if req.Method != http.MethodPost {
@ -34,27 +50,64 @@ func (ah *authHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
} else if pass, ok := ah.m[u]; !ok || pass != p { } else if pass, ok := ah.m[u]; !ok || pass != p {
http.Error(rw, "", http.StatusUnauthorized) http.Error(rw, "", http.StatusUnauthorized)
} else { } else {
_, e := rw.Write([]byte(`{"session_id": "hgx59wuWI3b0jcbtidv5mU1YCp-DOQ9NKR1iYKACdKCvbVn7"}`)) for _, e := range ah.entries {
if e != nil { if e.username == strings.ToLower(u) {
panic(e) _, err := rw.Write([]byte(fmt.Sprintf(`{"session_id": "%s"}`, e.sessionID)))
if err != nil {
panic(err)
} else {
return
} }
} }
} }
http.Error(rw, fmt.Sprintf("Do not find entry in entrylist, username: %s", u), http.StatusUnauthorized)
}
}
type reviewTokenHandler struct { type reviewTokenHandler struct {
entries []userEntry
} }
func (rth *reviewTokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (rth *reviewTokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost { if req.Method != http.MethodPost {
http.Error(rw, "", http.StatusMethodNotAllowed) http.Error(rw, "", http.StatusMethodNotAllowed)
} }
rw.Write([]byte(`{"apiVersion": "authentication.k8s.io/v1beta1", "kind": "TokenReview", "status": {"authenticated": true, "user": {"username": "administrator@vsphere.local", "groups": ["vsphere.local\\users", "vsphere.local\\administrators", "vsphere.local\\caadmins", "vsphere.local\\systemconfiguration.bashshelladministrators", "vsphere.local\\systemconfiguration.administrators", "vsphere.local\\licenseservice.administrators", "vsphere.local\\everyone"], "extra": {"method": ["basic"]}}}}`)) bodyBytes, err := ioutil.ReadAll(req.Body)
if err != nil {
http.Error(rw, fmt.Sprintf("failed to read request body, error: %v", err), http.StatusBadRequest)
}
reviewData := &v1beta1.TokenReview{}
if err := json.Unmarshal(bodyBytes, reviewData); err != nil {
http.Error(rw, fmt.Sprintf("failed to decode request body, error: %v", err), http.StatusBadRequest)
}
defer req.Body.Close()
for _, e := range rth.entries {
if reviewData.Spec.Token == e.sessionID {
_, err := rw.Write([]byte(fmt.Sprintf(reviewStatusTpl, e.username)))
if err != nil {
panic(err)
} else {
return
}
}
}
http.Error(rw, fmt.Sprintf("failed to match token: %s, entrylist: %+v", reviewData.Spec.Token, rth.entries), http.StatusUnauthorized)
} }
// NewMockServer creates the mock server for testing // NewMockServer creates the mock server for testing
func NewMockServer(creds map[string]string) *httptest.Server { func NewMockServer(creds map[string]string) *httptest.Server {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/test/login", &authHandler{m: creds}) entryList := []userEntry{}
mux.Handle("/test/tokenreview", &reviewTokenHandler{}) for user, pwd := range creds {
e := userEntry{
username: strings.ToLower(user),
password: pwd,
sessionID: utils.GenerateRandomString(),
reviewStatus: fmt.Sprintf(reviewStatusTpl, user),
}
entryList = append(entryList, e)
}
mux.Handle("/test/login", &authHandler{m: creds, entries: entryList})
mux.Handle("/test/tokenreview", &reviewTokenHandler{entries: entryList})
return httptest.NewTLSServer(mux) return httptest.NewTLSServer(mux)
} }

View File

@ -409,7 +409,6 @@ func HTTPAuthProxySetting() (*models.HTTPAuthProxy, error) {
VerifyCert: cfgMgr.Get(common.HTTPAuthProxyVerifyCert).GetBool(), VerifyCert: cfgMgr.Get(common.HTTPAuthProxyVerifyCert).GetBool(),
SkipSearch: cfgMgr.Get(common.HTTPAuthProxySkipSearch).GetBool(), SkipSearch: cfgMgr.Get(common.HTTPAuthProxySkipSearch).GetBool(),
ServerCertificate: cfgMgr.Get(common.HTTPAuthProxyServerCertificate).GetString(), ServerCertificate: cfgMgr.Get(common.HTTPAuthProxyServerCertificate).GetString(),
CaseSensitive: cfgMgr.Get(common.HTTPAuthProxyCaseSensitive).GetBool(),
}, nil }, nil
} }

View File

@ -241,7 +241,6 @@ y1bQusZMygQezfCuEzsewF+OpANFovCTUEs6s5vyoVNP8lk=
m := map[string]interface{}{ m := map[string]interface{}{
common.HTTPAuthProxySkipSearch: "true", common.HTTPAuthProxySkipSearch: "true",
common.HTTPAuthProxyVerifyCert: "true", common.HTTPAuthProxyVerifyCert: "true",
common.HTTPAuthProxyCaseSensitive: "false",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix", common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
common.HTTPAuthProxyServerCertificate: certificate, common.HTTPAuthProxyServerCertificate: certificate,
} }
@ -252,7 +251,6 @@ y1bQusZMygQezfCuEzsewF+OpANFovCTUEs6s5vyoVNP8lk=
Endpoint: "https://auth.proxy/suffix", Endpoint: "https://auth.proxy/suffix",
SkipSearch: true, SkipSearch: true,
VerifyCert: true, VerifyCert: true,
CaseSensitive: false,
ServerCertificate: certificate, ServerCertificate: certificate,
}) })
} }

View File

@ -257,7 +257,6 @@ func TestAuthProxyReqCtxModifier(t *testing.T) {
Endpoint: "https://auth.proxy/suffix", Endpoint: "https://auth.proxy/suffix",
SkipSearch: true, SkipSearch: true,
VerifyCert: false, VerifyCert: false,
CaseSensitive: true,
TokenReviewEndpoint: server.URL, TokenReviewEndpoint: server.URL,
}) })

View File

@ -132,7 +132,6 @@ y1bQusZMygQezfCuEzsewF+OpANFovCTUEs6s5vyoVNP8lk=
VerifyCert: false, VerifyCert: false,
SkipSearch: false, SkipSearch: false,
ServerCertificate: "", ServerCertificate: "",
CaseSensitive: false,
}, },
expect: rest.TLSClientConfig{ expect: rest.TLSClientConfig{
Insecure: true, Insecure: true,