Add configuration "case sensitive" to HTTP auth proxy

This commit make case sensitivity configurable when the authentication
backend is auth proxy.
When the "http_authproxy_case_sensitive" 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 regardless
upper or lower case is used.  It will be mapped to one entry in Harbor's
User/Group table.
Similar to auth_mode, there is limitation that once there are users
onboarded to Harbor's DB this attribute is not configurable.

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2019-11-05 19:35:25 +08:00
parent 54c5811974
commit 8933ab8074
9 changed files with 73 additions and 31 deletions

View File

@ -139,6 +139,7 @@ var (
{Name: common.HTTPAuthProxyTokenReviewEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}}, {Name: common.HTTPAuthProxyTokenReviewEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{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.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

@ -105,6 +105,7 @@ 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"
OIDCName = "oidc_name" OIDCName = "oidc_name"
OIDCEndpoint = "oidc_endpoint" OIDCEndpoint = "oidc_endpoint"
OIDCCLientID = "oidc_client_id" OIDCCLientID = "oidc_client_id"

View File

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

View File

@ -121,21 +121,32 @@ func (c *ConfigAPI) Put() {
} }
func (c *ConfigAPI) validateCfg(cfgs map[string]interface{}) (bool, error) { func (c *ConfigAPI) validateCfg(cfgs map[string]interface{}) (bool, error) {
mode := c.cfgManager.Get(common.AUTHMode).GetString()
if value, ok := cfgs[common.AUTHMode]; ok {
flag, err := authModeCanBeModified() flag, err := authModeCanBeModified()
if err != nil { if err != nil {
return true, err return true, err
} }
if mode != fmt.Sprintf("%v", value) && !flag { if !flag {
return false, fmt.Errorf("%s can not be modified as new users have been inserted into database", common.AUTHMode) if failedKeys := checkUnmodifiable(c.cfgManager, cfgs, common.AUTHMode, common.HTTPAuthProxyCaseSensitive); len(failedKeys) > 0 {
return false, fmt.Errorf("the keys %v can not be modified as new users have been inserted into database", failedKeys)
} }
} }
err := c.cfgManager.ValidateCfg(cfgs) err = c.cfgManager.ValidateCfg(cfgs)
if err != nil {
return false, err return false, err
} }
return false, nil
func checkUnmodifiable(mgr *config.CfgManager, cfgs map[string]interface{}, keys ...string) (failed []string) {
if mgr == nil || cfgs == nil || keys == nil {
return
}
for _, k := range keys {
v := mgr.Get(k).GetString()
if nv, ok := cfgs[k]; ok {
if v != fmt.Sprintf("%v", nv) {
failed = append(failed, k)
}
}
}
return
} }
// delete sensitive attrs and add editable field to every attr // delete sensitive attrs and add editable field to every attr

View File

@ -40,11 +40,14 @@ import (
const refreshDuration = 2 * time.Second const refreshDuration = 2 * time.Second
const userEntryComment = "By Authproxy" const userEntryComment = "By Authproxy"
var secureTransport = &http.Transport{} var secureTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
}
var insecureTransport = &http.Transport{ var insecureTransport = &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
}, },
Proxy: http.ProxyFromEnvironment,
} }
// Auth implements HTTP authenticator the required attributes. // Auth implements HTTP authenticator the required attributes.
@ -56,6 +59,10 @@ type Auth struct {
TokenReviewEndpoint string TokenReviewEndpoint string
SkipCertVerify bool SkipCertVerify bool
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
} }
@ -84,7 +91,8 @@ func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
user := &models.User{Username: m.Principal} 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)
@ -141,6 +149,7 @@ func (a *Auth) tokenReview(sessionID string) (*k8s_api_v1beta1.TokenReview, erro
// 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)
} }
@ -156,12 +165,14 @@ func (a *Auth) PostAuthenticate(u *models.User) error {
} }
// SearchUser returns nil as authproxy does not have such capability. // SearchUser returns nil as authproxy does not have such capability.
// When SkipSearch is set it always return the default model. // When SkipSearch is set it always return the default model,
// the username will be switch to lowercase if it's configured as case-insensitive
func (a *Auth) SearchUser(username string) (*models.User, error) { func (a *Auth) SearchUser(username string) (*models.User, error) {
err := a.ensure() err := a.ensure()
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}
@ -178,6 +189,7 @@ 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{
@ -196,6 +208,7 @@ 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
@ -241,6 +254,13 @@ func (a *Auth) ensure() error {
return nil return 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

@ -43,11 +43,11 @@ func TestMain(m *testing.M) {
} }
mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"}) mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"})
defer mockSvr.Close() defer mockSvr.Close()
defer dao.ExecuteBatchSQL([]string{"delete from user_group where group_name='OnBoardTest'"})
a = &Auth{ a = &Auth{
Endpoint: mockSvr.URL + "/test/login", Endpoint: mockSvr.URL + "/test/login",
TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview", TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview",
SkipCertVerify: true, SkipCertVerify: true,
CaseSensitive: false,
// So it won't require mocking the cfgManager // So it won't require mocking the cfgManager
settingTimeStamp: time.Now(), settingTimeStamp: time.Now(),
} }
@ -56,6 +56,7 @@ func TestMain(m *testing.M) {
common.HTTPAuthProxyEndpoint: a.Endpoint, common.HTTPAuthProxyEndpoint: a.Endpoint,
common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint, common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint,
common.HTTPAuthProxyVerifyCert: !a.SkipCertVerify, common.HTTPAuthProxyVerifyCert: !a.SkipCertVerify,
common.HTTPAuthProxyCaseSensitive: a.CaseSensitive,
common.PostGreSQLSSLMode: cfgMap[common.PostGreSQLSSLMode], common.PostGreSQLSSLMode: cfgMap[common.PostGreSQLSSLMode],
common.PostGreSQLUsername: cfgMap[common.PostGreSQLUsername], common.PostGreSQLUsername: cfgMap[common.PostGreSQLUsername],
common.PostGreSQLPort: cfgMap[common.PostGreSQLPort], common.PostGreSQLPort: cfgMap[common.PostGreSQLPort],
@ -65,6 +66,7 @@ func TestMain(m *testing.M) {
} }
config.InitWithSettings(conf) config.InitWithSettings(conf)
defer dao.ExecuteBatchSQL([]string{"delete from user_group where group_name='onboardtest'"})
rc := m.Run() rc := m.Run()
if err := dao.ClearHTTPAuthProxyUsers(); err != nil { if err := dao.ClearHTTPAuthProxyUsers(); err != nil {
panic(err) panic(err)
@ -117,7 +119,7 @@ func TestAuth_Authenticate(t *testing.T) {
}, },
expect: output{ expect: output{
user: models.User{ user: models.User{
Username: "Admin@vsphere.local", Username: "admin@vsphere.local",
GroupIDs: groupIDs, GroupIDs: groupIDs,
// Email: "Admin@placeholder.com", // Email: "Admin@placeholder.com",
// Password: pwd, // Password: pwd,
@ -172,12 +174,12 @@ func TestAuth_PostAuthenticate(t *testing.T) {
}, },
{ {
input: &models.User{ input: &models.User{
Username: "Admin@vsphere.local", Username: "admin@vsphere.local",
}, },
expect: models.User{ expect: models.User{
Username: "Admin@vsphere.local", Username: "admin@vsphere.local",
Email: "Admin@vsphere.local", Email: "admin@vsphere.local",
Realname: "Admin@vsphere.local", Realname: "admin@vsphere.local",
Password: pwd, Password: pwd,
Comment: userEntryComment, Comment: userEntryComment,
}, },
@ -201,6 +203,9 @@ func TestAuth_OnBoardGroup(t *testing.T) {
a.OnBoardGroup(input, "") a.OnBoardGroup(input, "")
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)
assert.Nil(t, er)
assert.Equal(t, "onboardtest", g.GroupName)
emptyGroup := &models.UserGroup{} emptyGroup := &models.UserGroup{}
err := a.OnBoardGroup(emptyGroup, "") err := a.OnBoardGroup(emptyGroup, "")

View File

@ -475,8 +475,8 @@ func HTTPAuthProxySetting() (*models.HTTPAuthProxy, error) {
TokenReviewEndpoint: cfgMgr.Get(common.HTTPAuthProxyTokenReviewEndpoint).GetString(), TokenReviewEndpoint: cfgMgr.Get(common.HTTPAuthProxyTokenReviewEndpoint).GetString(),
VerifyCert: cfgMgr.Get(common.HTTPAuthProxyVerifyCert).GetBool(), VerifyCert: cfgMgr.Get(common.HTTPAuthProxyVerifyCert).GetBool(),
SkipSearch: cfgMgr.Get(common.HTTPAuthProxySkipSearch).GetBool(), SkipSearch: cfgMgr.Get(common.HTTPAuthProxySkipSearch).GetBool(),
CaseSensitive: cfgMgr.Get(common.HTTPAuthProxyCaseSensitive).GetBool(),
}, nil }, nil
} }
// OIDCSetting returns the setting of OIDC provider, currently there's only one OIDC provider allowed for Harbor and it's // OIDCSetting returns the setting of OIDC provider, currently there's only one OIDC provider allowed for Harbor and it's

View File

@ -219,6 +219,7 @@ func TestHTTPAuthProxySetting(t *testing.T) {
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",
} }
InitWithSettings(m) InitWithSettings(m)
@ -228,6 +229,7 @@ func TestHTTPAuthProxySetting(t *testing.T) {
Endpoint: "https://auth.proxy/suffix", Endpoint: "https://auth.proxy/suffix",
SkipSearch: true, SkipSearch: true,
VerifyCert: true, VerifyCert: true,
CaseSensitive: false,
}) })
} }

View File

@ -257,6 +257,7 @@ 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,
}) })