Merge pull request #6771 from reasonerjt/authproxy-authn

Provide HTTP authenticator
This commit is contained in:
Wenkai Yin 2019-01-28 17:44:15 +08:00 committed by GitHub
commit dffe824c84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 340 additions and 2 deletions

View File

@ -19,6 +19,7 @@ const (
DBAuth = "db_auth"
LDAPAuth = "ldap_auth"
UAAAuth = "uaa_auth"
HTTPAuth = "http_auth"
ProCrtRestrEveryone = "everyone"
ProCrtRestrAdmOnly = "adminonly"
LDAPScopeBase = 0

View File

@ -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.

View 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{})
}

View 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)
}
}
*/

View 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)
}

View File

@ -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"

View File

@ -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"