mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 17:47:46 +01:00
Provide HTTP authenticator
An HTTP authenticator verifies the credentials by sending a POST request to an HTTP endpoint. After successful authentication he will be onboarded to Harbor's local DB and assigned a role in a project. This commit provides the initial implementation. Currently one limitation is that we don't have clear definition about how we would "search" a user via this HTTP authenticator, a flag for "alway onboard" is provided to skip the search, otherwise, a user has to login first before he can be assigned a role in Harbor. Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
parent
6888c3247c
commit
20db0e737b
@ -19,6 +19,7 @@ const (
|
||||
DBAuth = "db_auth"
|
||||
LDAPAuth = "ldap_auth"
|
||||
UAAAuth = "uaa_auth"
|
||||
HTTPAuth = "http_auth"
|
||||
ProCrtRestrEveryone = "everyone"
|
||||
ProCrtRestrAdmOnly = "adminonly"
|
||||
LDAPScopeBase = 0
|
||||
|
@ -123,7 +123,7 @@ func Register(name string, h AuthenticateHelper) {
|
||||
return
|
||||
}
|
||||
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.
|
||||
|
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 {
|
||||
user.Username = strings.TrimSpace(user.Username)
|
||||
if len(user.Username) == 0 {
|
||||
return fmt.Errorf("The Username is empty")
|
||||
return fmt.Errorf("the Username is empty")
|
||||
}
|
||||
if len(user.Password) == 0 {
|
||||
user.Password = "1234567ab"
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"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/ldap"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/uaa"
|
||||
|
Loading…
Reference in New Issue
Block a user