mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-18 16:25:16 +01:00
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:
parent
54c5811974
commit
8933ab8074
@ -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{}},
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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{})
|
||||||
}
|
}
|
||||||
|
@ -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, "")
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user