Move Settings of HTTP auth proxy (#7047)

Previously the settings of HTTP authproxy were set in environment
variable.
This commit move them to the configuration API

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2019-03-01 14:11:14 +08:00 committed by Yan
parent f79895b6d8
commit 321874c815
9 changed files with 162 additions and 77 deletions

View File

@ -30,7 +30,7 @@ type Item struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// It can be &IntType{}, &StringType{}, &BoolType{}, &PasswordType{}, &MapType{} etc, any type interface implementation // It can be &IntType{}, &StringType{}, &BoolType{}, &PasswordType{}, &MapType{} etc, any type interface implementation
ItemType Type ItemType Type
// Is this settign can be modified after configure // TODO: Clarify the usage of this attribute
Editable bool `json:"editable,omitempty"` Editable bool `json:"editable,omitempty"`
} }
@ -44,6 +44,7 @@ const (
LdapGroupGroup = "ldapgroup" LdapGroupGroup = "ldapgroup"
EmailGroup = "email" EmailGroup = "email"
UAAGroup = "uaa" UAAGroup = "uaa"
HTTPAuthGroup = "http_auth"
DatabaseGroup = "database" DatabaseGroup = "database"
// Put all config items do not belong a existing group into basic // Put all config items do not belong a existing group into basic
BasicGroup = "basic" BasicGroup = "basic"
@ -57,6 +58,7 @@ var (
// 2. Get/Set config settings by CfgManager // 2. Get/Set config settings by CfgManager
// 3. CfgManager.Load()/CfgManager.Save() to load/save from configure storage. // 3. CfgManager.Load()/CfgManager.Save() to load/save from configure storage.
ConfigList = []Item{ ConfigList = []Item{
// TODO: All these Name should be reference to const, see #7040
{Name: "admin_initial_password", Scope: SystemScope, Group: BasicGroup, EnvKey: "HARBOR_ADMIN_PASSWORD", DefaultValue: "", ItemType: &PasswordType{}, Editable: true}, {Name: "admin_initial_password", Scope: SystemScope, Group: BasicGroup, EnvKey: "HARBOR_ADMIN_PASSWORD", DefaultValue: "", ItemType: &PasswordType{}, Editable: true},
{Name: "admiral_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "ADMIRAL_URL", DefaultValue: "", ItemType: &StringType{}, Editable: false}, {Name: "admiral_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "ADMIRAL_URL", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "auth_mode", Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &AuthModeType{}, Editable: false}, {Name: "auth_mode", Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &AuthModeType{}, Editable: false},
@ -127,6 +129,10 @@ var (
{Name: "uaa_endpoint", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_ENDPOINT", DefaultValue: "", ItemType: &StringType{}, Editable: false}, {Name: "uaa_endpoint", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_ENDPOINT", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: "uaa_verify_cert", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_VERIFY_CERT", DefaultValue: "false", ItemType: &BoolType{}, Editable: false}, {Name: "uaa_verify_cert", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_VERIFY_CERT", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: common.HTTPAuthProxyEndpoint, Scope: UserScope, Group: HTTPAuthGroup, EnvKey: "HTTP_AUTHPROXY_ENDPOINT", DefaultValue: "", ItemType: &StringType{}, Editable: false},
{Name: common.HTTPAuthProxySkipCertVerify, Scope: UserScope, Group: HTTPAuthGroup, EnvKey: "HTTP_AUTHPROXY_SKIP_CERT_VERIFY", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: common.HTTPAuthProxyAlwaysOnboard, Scope: UserScope, Group: HTTPAuthGroup, EnvKey: "HTTP_AUTHPROXY_ALWAYS_ONBOARD", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, {Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
{Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, {Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
{Name: "with_notary", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_NOTARY", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, {Name: "with_notary", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_NOTARY", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},

View File

@ -61,11 +61,11 @@ type AuthModeType struct {
} }
func (t *AuthModeType) validate(str string) error { func (t *AuthModeType) validate(str string) error {
if str == common.LDAPAuth || str == common.DBAuth || str == common.UAAAuth { if str == common.LDAPAuth || str == common.DBAuth || str == common.UAAAuth || str == common.HTTPAuth {
return nil return nil
} }
return fmt.Errorf("invalid %s, shoud be one of %s, %s, %s", return fmt.Errorf("invalid %s, shoud be one of %s, %s, %s, %s",
common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth) common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth, common.HTTPAuth)
} }
// ProjectCreationRestrictionType ... // ProjectCreationRestrictionType ...

View File

@ -41,60 +41,64 @@ const (
ResourceTypeImage = "i" ResourceTypeImage = "i"
ResourceTypeChart = "c" ResourceTypeChart = "c"
ExtEndpoint = "ext_endpoint" ExtEndpoint = "ext_endpoint"
AUTHMode = "auth_mode" AUTHMode = "auth_mode"
DatabaseType = "database_type" DatabaseType = "database_type"
PostGreSQLHOST = "postgresql_host" PostGreSQLHOST = "postgresql_host"
PostGreSQLPort = "postgresql_port" PostGreSQLPort = "postgresql_port"
PostGreSQLUsername = "postgresql_username" PostGreSQLUsername = "postgresql_username"
PostGreSQLPassword = "postgresql_password" PostGreSQLPassword = "postgresql_password"
PostGreSQLDatabase = "postgresql_database" PostGreSQLDatabase = "postgresql_database"
PostGreSQLSSLMode = "postgresql_sslmode" PostGreSQLSSLMode = "postgresql_sslmode"
SelfRegistration = "self_registration" SelfRegistration = "self_registration"
CoreURL = "core_url" CoreURL = "core_url"
JobServiceURL = "jobservice_url" JobServiceURL = "jobservice_url"
LDAPURL = "ldap_url" LDAPURL = "ldap_url"
LDAPSearchDN = "ldap_search_dn" LDAPSearchDN = "ldap_search_dn"
LDAPSearchPwd = "ldap_search_password" LDAPSearchPwd = "ldap_search_password"
LDAPBaseDN = "ldap_base_dn" LDAPBaseDN = "ldap_base_dn"
LDAPUID = "ldap_uid" LDAPUID = "ldap_uid"
LDAPFilter = "ldap_filter" LDAPFilter = "ldap_filter"
LDAPScope = "ldap_scope" LDAPScope = "ldap_scope"
LDAPTimeout = "ldap_timeout" LDAPTimeout = "ldap_timeout"
LDAPVerifyCert = "ldap_verify_cert" LDAPVerifyCert = "ldap_verify_cert"
LDAPGroupBaseDN = "ldap_group_base_dn" LDAPGroupBaseDN = "ldap_group_base_dn"
LDAPGroupSearchFilter = "ldap_group_search_filter" LDAPGroupSearchFilter = "ldap_group_search_filter"
LDAPGroupAttributeName = "ldap_group_attribute_name" LDAPGroupAttributeName = "ldap_group_attribute_name"
LDAPGroupSearchScope = "ldap_group_search_scope" LDAPGroupSearchScope = "ldap_group_search_scope"
TokenServiceURL = "token_service_url" TokenServiceURL = "token_service_url"
RegistryURL = "registry_url" RegistryURL = "registry_url"
EmailHost = "email_host" EmailHost = "email_host"
EmailPort = "email_port" EmailPort = "email_port"
EmailUsername = "email_username" EmailUsername = "email_username"
EmailPassword = "email_password" EmailPassword = "email_password"
EmailFrom = "email_from" EmailFrom = "email_from"
EmailSSL = "email_ssl" EmailSSL = "email_ssl"
EmailIdentity = "email_identity" EmailIdentity = "email_identity"
EmailInsecure = "email_insecure" EmailInsecure = "email_insecure"
ProjectCreationRestriction = "project_creation_restriction" ProjectCreationRestriction = "project_creation_restriction"
MaxJobWorkers = "max_job_workers" MaxJobWorkers = "max_job_workers"
TokenExpiration = "token_expiration" TokenExpiration = "token_expiration"
CfgExpiration = "cfg_expiration" CfgExpiration = "cfg_expiration"
AdminInitialPassword = "admin_initial_password" AdminInitialPassword = "admin_initial_password"
AdmiralEndpoint = "admiral_url" AdmiralEndpoint = "admiral_url"
WithNotary = "with_notary" WithNotary = "with_notary"
WithClair = "with_clair" WithClair = "with_clair"
ScanAllPolicy = "scan_all_policy" ScanAllPolicy = "scan_all_policy"
ClairDBPassword = "clair_db_password" ClairDBPassword = "clair_db_password"
ClairDBHost = "clair_db_host" ClairDBHost = "clair_db_host"
ClairDBPort = "clair_db_port" ClairDBPort = "clair_db_port"
ClairDB = "clair_db" ClairDB = "clair_db"
ClairDBUsername = "clair_db_username" ClairDBUsername = "clair_db_username"
ClairDBSSLMode = "clair_db_sslmode" ClairDBSSLMode = "clair_db_sslmode"
UAAEndpoint = "uaa_endpoint" UAAEndpoint = "uaa_endpoint"
UAAClientID = "uaa_client_id" UAAClientID = "uaa_client_id"
UAAClientSecret = "uaa_client_secret" UAAClientSecret = "uaa_client_secret"
UAAVerifyCert = "uaa_verify_cert" UAAVerifyCert = "uaa_verify_cert"
HTTPAuthProxyEndpoint = "http_authproxy_endpoint"
HTTPAuthProxySkipCertVerify = "http_authproxy_skip_cert_verify"
HTTPAuthProxyAlwaysOnboard = "http_authproxy_always_onboard"
DefaultClairEndpoint = "http://clair:6060" DefaultClairEndpoint = "http://clair:6060"
CfgDriverDB = "db" CfgDriverDB = "db"
NewHarborAdminName = "admin@harbor.local" NewHarborAdminName = "admin@harbor.local"
@ -107,7 +111,6 @@ const (
DefaultCoreEndpoint = "http://core:8080" DefaultCoreEndpoint = "http://core:8080"
DefaultNotaryEndpoint = "http://notary-server:4443" DefaultNotaryEndpoint = "http://notary-server:4443"
LdapGroupType = 1 LdapGroupType = 1
ReloadKey = "reload_key"
LdapGroupAdminDn = "ldap_group_admin_dn" LdapGroupAdminDn = "ldap_group_admin_dn"
DefaultRegistryControllerEndpoint = "http://registryctl:8080" DefaultRegistryControllerEndpoint = "http://registryctl:8080"
WithChartMuseum = "with_chartmuseum" WithChartMuseum = "with_chartmuseum"

View File

@ -133,3 +133,12 @@ func ArrayEqual(arrayA, arrayB []int) bool {
} }
return true return true
} }
// ClearHTTPAuthProxyUsers remove the records from harbor_users to delete all user imported via
// HTTP Auth Proxy
func ClearHTTPAuthProxyUsers() error {
o := GetOrmer()
sql := "DELETE FROM harbor_user WHERE comment='By Authproxy'"
_, err := o.Raw(sql).Exec()
return err
}

View File

@ -65,6 +65,13 @@ type Email struct {
Insecure bool `json:"insecure"` Insecure bool `json:"insecure"`
} }
// HTTPAuthProxy wraps the settings for HTTP auth proxy
type HTTPAuthProxy struct {
Endpoint string `json:"endpoint"`
SkipCertVerify bool `json:"skip_cert_verify"`
AlwaysOnBoard bool `json:"always_onboard"`
}
/* /*
// Registry ... // Registry ...
type Registry struct { type Registry struct {

View File

@ -22,27 +22,38 @@ import (
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/auth" "github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/core/config"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"strings" "strings"
"sync" "sync"
"time"
) )
const refreshDuration = 5 * time.Second
const userEntryComment = "By Authproxy"
// Auth implements HTTP authenticator the required attributes. // Auth implements HTTP authenticator the required attributes.
// The attribute Endpoint is the HTTP endpoint to which the POST request should be issued for authentication // The attribute Endpoint is the HTTP endpoint to which the POST request should be issued for authentication
type Auth struct { type Auth struct {
auth.DefaultAuthenticateHelper auth.DefaultAuthenticateHelper
sync.Mutex sync.Mutex
Endpoint string Endpoint string
SkipCertVerify bool SkipCertVerify bool
AlwaysOnboard bool AlwaysOnboard bool
client *http.Client settingTimeStamp time.Time
client *http.Client
} }
// Authenticate issues http POST request to Endpoint if it returns 200 the authentication is considered success. // 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) { func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
a.ensure() err := a.ensure()
if err != nil {
if a.Endpoint == "" {
return nil, fmt.Errorf("failed to initialize HTTP Auth Proxy Authenticator, error: %v", err)
}
log.Warningf("Failed to refresh configuration for HTTP Auth Proxy Authenticator, error: %v, old settings will be used", err)
}
req, err := http.NewRequest(http.MethodPost, a.Endpoint, nil) req, err := http.NewRequest(http.MethodPost, a.Endpoint, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to send request, error: %v", err) return nil, fmt.Errorf("failed to send request, error: %v", err)
@ -109,7 +120,7 @@ func (a *Auth) fillInModel(u *models.User) error {
} }
u.Realname = u.Username u.Realname = u.Username
u.Password = "1234567ab" u.Password = "1234567ab"
u.Comment = "By Authproxy" u.Comment = userEntryComment
if strings.Contains(u.Username, "@") { if strings.Contains(u.Username, "@") {
u.Email = u.Username u.Email = u.Username
} else { } else {
@ -118,13 +129,17 @@ func (a *Auth) fillInModel(u *models.User) error {
return nil return nil
} }
func (a *Auth) ensure() { func (a *Auth) ensure() error {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
if a.Endpoint == "" { if time.Now().Sub(a.settingTimeStamp) >= refreshDuration {
a.Endpoint = os.Getenv("AUTHPROXY_ENDPOINT") setting, err := config.HTTPAuthProxySetting()
a.SkipCertVerify = strings.EqualFold(os.Getenv("AUTHPROXY_SKIP_CERT_VERIFY"), "true") if err != nil {
a.AlwaysOnboard = strings.EqualFold(os.Getenv("AUTHPROXY_ALWAYS_ONBOARD"), "true") return err
}
a.Endpoint = setting.Endpoint
a.SkipCertVerify = setting.SkipCertVerify
a.AlwaysOnboard = setting.AlwaysOnBoard
} }
if a.client == nil { if a.client == nil {
tr := &http.Transport{ tr := &http.Transport{
@ -136,6 +151,7 @@ func (a *Auth) ensure() {
Transport: tr, Transport: tr,
} }
} }
return nil
} }
func init() { func init() {

View File

@ -15,13 +15,16 @@
package authproxy package authproxy
import ( import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
cut "github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/auth" "github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/core/auth/authproxy/test" "github.com/goharbor/harbor/src/core/auth/authproxy/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http/httptest" "net/http/httptest"
"os" "os"
"testing" "testing"
"time"
) )
var mockSvr *httptest.Server var mockSvr *httptest.Server
@ -30,13 +33,22 @@ var pwd = "1234567ab"
var cmt = "By Authproxy" var cmt = "By Authproxy"
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
cut.InitDatabaseFromEnv()
if err := dao.ClearHTTPAuthProxyUsers(); err != nil {
panic(err)
}
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()
a = &Auth{ a = &Auth{
Endpoint: mockSvr.URL + "/test/login", Endpoint: mockSvr.URL + "/test/login",
SkipCertVerify: true, SkipCertVerify: true,
// So it won't require mocking the cfgManager
settingTimeStamp: time.Now(),
} }
rc := m.Run() rc := m.Run()
if err := dao.ClearHTTPAuthProxyUsers(); err != nil {
panic(err)
}
if rc != 0 { if rc != 0 {
os.Exit(rc) os.Exit(rc)
} }
@ -104,7 +116,6 @@ func TestAuth_Authenticate(t *testing.T) {
} }
} }
/* TODO: Enable this case after adminserver refactor is merged.
func TestAuth_PostAuthenticate(t *testing.T) { func TestAuth_PostAuthenticate(t *testing.T) {
type tc struct { type tc struct {
input *models.User input *models.User
@ -120,7 +131,7 @@ func TestAuth_PostAuthenticate(t *testing.T) {
Email: "jt@placeholder.com", Email: "jt@placeholder.com",
Realname: "jt", Realname: "jt",
Password: pwd, Password: pwd,
Comment: fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"), Comment: userEntryComment,
}, },
}, },
{ {
@ -129,16 +140,19 @@ func TestAuth_PostAuthenticate(t *testing.T) {
}, },
expect: models.User{ expect: models.User{
Username: "Admin@vsphere.local", Username: "Admin@vsphere.local",
Email: "jt@placeholder.com", Email: "Admin@vsphere.local",
Realname: "Admin@vsphere.local", Realname: "Admin@vsphere.local",
Password: pwd, Password: pwd,
Comment: fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"), Comment: userEntryComment,
}, },
}, },
} }
for _, c := range suite { for _, c := range suite {
a.PostAuthenticate(c.input) a.PostAuthenticate(c.input)
assert.Equal(t, c.expect, *c.input) assert.Equal(t, c.expect.Username, c.input.Username)
assert.Equal(t, c.expect.Email, c.input.Email)
assert.Equal(t, c.expect.Realname, c.input.Realname)
assert.Equal(t, c.expect.Comment, c.input.Comment)
} }
} }
*/

View File

@ -462,3 +462,17 @@ func GetClairHealthCheckServerURL() string {
} }
return url return url
} }
// HTTPAuthProxySetting returns the setting of HTTP Auth proxy. the settings are only meaningful when the auth_mode is
// set to http_auth
func HTTPAuthProxySetting() (*models.HTTPAuthProxy, error) {
if err := cfgMgr.Load(); err != nil {
return nil, err
}
return &models.HTTPAuthProxy{
Endpoint: cfgMgr.Get(common.HTTPAuthProxyEndpoint).GetString(),
SkipCertVerify: cfgMgr.Get(common.HTTPAuthProxySkipCertVerify).GetBool(),
AlwaysOnBoard: cfgMgr.Get(common.HTTPAuthProxyAlwaysOnboard).GetBool(),
}, nil
}

View File

@ -225,3 +225,19 @@ func TestConfigureValue_GetMap(t *testing.T) {
} }
fmt.Printf("%+v\n", policy) fmt.Printf("%+v\n", policy)
} }
func TestHTTPAuthProxySetting(t *testing.T) {
m := map[string]interface{}{
common.HTTPAuthProxyAlwaysOnboard: "true",
common.HTTPAuthProxySkipCertVerify: "true",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
}
InitWithSettings(m)
v, e := HTTPAuthProxySetting()
assert.Nil(t, e)
assert.Equal(t, *v, models.HTTPAuthProxy{
Endpoint: "https://auth.proxy/suffix",
AlwaysOnBoard: true,
SkipCertVerify: true,
})
}