mirror of
https://github.com/goharbor/harbor
synced 2025-05-14 23:32:28 +00:00
Merge pull request #6771 from reasonerjt/authproxy-authn
Provide HTTP authenticator
This commit is contained in:
commit
dffe824c84
@ -19,6 +19,7 @@ const (
|
|||||||
DBAuth = "db_auth"
|
DBAuth = "db_auth"
|
||||||
LDAPAuth = "ldap_auth"
|
LDAPAuth = "ldap_auth"
|
||||||
UAAAuth = "uaa_auth"
|
UAAAuth = "uaa_auth"
|
||||||
|
HTTPAuth = "http_auth"
|
||||||
ProCrtRestrEveryone = "everyone"
|
ProCrtRestrEveryone = "everyone"
|
||||||
ProCrtRestrAdmOnly = "adminonly"
|
ProCrtRestrAdmOnly = "adminonly"
|
||||||
LDAPScopeBase = 0
|
LDAPScopeBase = 0
|
||||||
|
@ -123,7 +123,7 @@ func Register(name string, h AuthenticateHelper) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
registry[name] = h
|
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.
|
// Login authenticates user credentials based on setting.
|
||||||
|
143
src/core/auth/authproxy/auth.go
Normal file
143
src/core/auth/authproxy/auth.go
Normal file
@ -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{})
|
||||||
|
}
|
144
src/core/auth/authproxy/auth_test.go
Normal file
144
src/core/auth/authproxy/auth_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
49
src/core/auth/authproxy/test/server.go
Normal file
49
src/core/auth/authproxy/test/server.go
Normal file
@ -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 {
|
func (u *Auth) OnBoardUser(user *models.User) error {
|
||||||
user.Username = strings.TrimSpace(user.Username)
|
user.Username = strings.TrimSpace(user.Username)
|
||||||
if len(user.Username) == 0 {
|
if len(user.Username) == 0 {
|
||||||
return fmt.Errorf("The Username is empty")
|
return fmt.Errorf("the Username is empty")
|
||||||
}
|
}
|
||||||
if len(user.Password) == 0 {
|
if len(user.Password) == 0 {
|
||||||
user.Password = "1234567ab"
|
user.Password = "1234567ab"
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/api"
|
"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/db"
|
||||||
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
||||||
_ "github.com/goharbor/harbor/src/core/auth/uaa"
|
_ "github.com/goharbor/harbor/src/core/auth/uaa"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user