Read Email from UAA while onboarding user.

Will call the userinfo API of UAA to get user info and generage user
model based on the response.  Also this commit include a change that
whenever the UAA Client is to be used it will update the configuraiton,
this is needed as we enable user to update the configuration of UAA via
UI.
This commit is contained in:
Tan Jiang 2018-01-16 22:18:56 +08:00
parent a9fe5564df
commit d5d913f51d
5 changed files with 117 additions and 43 deletions

View File

@ -41,6 +41,8 @@ const (
UsersURLSuffix = "/Users" UsersURLSuffix = "/Users"
) )
var uaaTransport = &http.Transport{}
// Client provides funcs to interact with UAA. // Client provides funcs to interact with UAA.
type Client interface { type Client interface {
//PasswordAuth accepts username and password, return a token if it's valid. //PasswordAuth accepts username and password, return a token if it's valid.
@ -49,6 +51,8 @@ type Client interface {
GetUserInfo(token string) (*UserInfo, error) GetUserInfo(token string) (*UserInfo, error)
//SearchUser searches a user based on user name. //SearchUser searches a user based on user name.
SearchUser(name string) ([]*SearchUserEntry, error) SearchUser(name string) ([]*SearchUserEntry, error)
//UpdateConfig updates the config of the current client
UpdateConfig(cfg *ClientConfig) error
} }
// ClientConfig values to initialize UAA Client // ClientConfig values to initialize UAA Client
@ -169,13 +173,13 @@ func (dc *defaultClient) prepareCtx() context.Context {
return context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient) return context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient)
} }
// NewDefaultClient creates an instance of defaultClient. func (dc *defaultClient) UpdateConfig(cfg *ClientConfig) error {
func NewDefaultClient(cfg *ClientConfig) (Client, error) {
url := cfg.Endpoint url := cfg.Endpoint
if !strings.Contains(url, "://") { if !strings.Contains(url, "://") {
url = "https://" + url url = "https://" + url
} }
url = strings.TrimSuffix(url, "/") url = strings.TrimSuffix(url, "/")
dc.endpoint = url
tc := &tls.Config{ tc := &tls.Config{
InsecureSkipVerify: cfg.SkipTLSVerify, InsecureSkipVerify: cfg.SkipTLSVerify,
} }
@ -183,7 +187,7 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
if _, err := os.Stat(cfg.CARootPath); !os.IsNotExist(err) { if _, err := os.Stat(cfg.CARootPath); !os.IsNotExist(err) {
content, err := ioutil.ReadFile(cfg.CARootPath) content, err := ioutil.ReadFile(cfg.CARootPath)
if err != nil { if err != nil {
return nil, err return err
} }
pool := x509.NewCertPool() pool := x509.NewCertPool()
//Do not throw error if the certificate is malformed, so we can put a place holder. //Do not throw error if the certificate is malformed, so we can put a place holder.
@ -196,11 +200,9 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
log.Warningf("The root certificate file %s is not found, skip configuring root cert in UAA client.", cfg.CARootPath) log.Warningf("The root certificate file %s is not found, skip configuring root cert in UAA client.", cfg.CARootPath)
} }
} }
hc := &http.Client{ uaaTransport.TLSClientConfig = tc
Transport: &http.Transport{ dc.httpClient.Transport = uaaTransport
TLSClientConfig: tc, //dc.httpClient.Transport = transport.
},
}
oc := &oauth2.Config{ oc := &oauth2.Config{
ClientID: cfg.ClientID, ClientID: cfg.ClientID,
@ -216,11 +218,17 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
ClientSecret: cfg.ClientSecret, ClientSecret: cfg.ClientSecret,
TokenURL: url + TokenURLSuffix, TokenURL: url + TokenURLSuffix,
} }
dc.oauth2Cfg = oc
return &defaultClient{ dc.twoLegCfg = cc
httpClient: hc, return nil
oauth2Cfg: oc, }
twoLegCfg: cc,
endpoint: url, // NewDefaultClient creates an instance of defaultClient.
}, nil func NewDefaultClient(cfg *ClientConfig) (Client, error) {
hc := &http.Client{}
c := &defaultClient{httpClient: hc}
if err := c.UpdateConfig(cfg); err != nil {
return nil, err
}
return c, nil
} }

View File

@ -19,6 +19,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
const fakeToken = "The Fake Token"
// FakeClient is for test only // FakeClient is for test only
type FakeClient struct { type FakeClient struct {
Username string Username string
@ -28,14 +30,26 @@ type FakeClient struct {
// PasswordAuth ... // PasswordAuth ...
func (fc *FakeClient) PasswordAuth(username, password string) (*oauth2.Token, error) { func (fc *FakeClient) PasswordAuth(username, password string) (*oauth2.Token, error) {
if username == fc.Username && password == fc.Password { if username == fc.Username && password == fc.Password {
return &oauth2.Token{}, nil return &oauth2.Token{AccessToken: fakeToken}, nil
} }
return nil, fmt.Errorf("Invalide username and password") return nil, fmt.Errorf("Invalide username and password")
} }
// GetUserInfo ... // GetUserInfo ...
func (fc *FakeClient) GetUserInfo(token string) (*UserInfo, error) { func (fc *FakeClient) GetUserInfo(token string) (*UserInfo, error) {
return nil, nil if token != fakeToken {
return nil, fmt.Errorf("Unexpected token: %s, expected: %s", token, fakeToken)
}
info := &UserInfo{
Name: "fakeName",
Email: "fake@fake.com",
}
return info, nil
}
// UpdateConfig ...
func (fc *FakeClient) UpdateConfig(cfg *ClientConfig) error {
return nil
} }
// SearchUser ... // SearchUser ...

View File

@ -110,7 +110,7 @@ func Login(m models.AuthModel) (*models.User, error) {
time.Sleep(frozenTime) time.Sleep(frozenTime)
} }
authenticator.PostAuthenticate(user) err = authenticator.PostAuthenticate(user)
return user, err return user, err
} }

View File

@ -23,27 +23,12 @@ import (
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/uaa" "github.com/vmware/harbor/src/common/utils/uaa"
"github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
) )
//CreateClient create a UAA Client instance based on system configuration.
func CreateClient() (uaa.Client, error) {
UAASettings, err := config.UAASettings()
if err != nil {
return nil, err
}
cfg := &uaa.ClientConfig{
ClientID: UAASettings.ClientID,
ClientSecret: UAASettings.ClientSecret,
Endpoint: UAASettings.Endpoint,
SkipTLSVerify: !UAASettings.VerifyCert,
CARootPath: os.Getenv("UAA_CA_ROOT"),
}
return uaa.NewDefaultClient(cfg)
}
// Auth is the implementation of AuthenticateHelper to access uaa for authentication. // Auth is the implementation of AuthenticateHelper to access uaa for authentication.
type Auth struct { type Auth struct {
sync.Mutex sync.Mutex
@ -58,12 +43,17 @@ func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
} }
t, err := u.client.PasswordAuth(m.Principal, m.Password) t, err := u.client.PasswordAuth(m.Principal, m.Password)
if t != nil && err == nil { if t != nil && err == nil {
//TODO: See if it's possible to get more information from token.
user := &models.User{ user := &models.User{
Username: m.Principal, Username: m.Principal,
} }
err = u.OnBoardUser(user) info, err2 := u.client.GetUserInfo(t.AccessToken)
return user, err if err2 != nil {
log.Warningf("Failed to extract user info from UAA, error: %v", err2)
} else {
user.Email = info.Email
user.Realname = info.Name
}
return user, nil
} }
return nil, err return nil, err
} }
@ -89,6 +79,28 @@ func (u *Auth) OnBoardUser(user *models.User) error {
return dao.OnBoardUser(user) return dao.OnBoardUser(user)
} }
// PostAuthenticate will check if user exists in DB, if not on Board user, if he does, update the profile.
func (u *Auth) PostAuthenticate(user *models.User) error {
dbUser, err := dao.GetUser(models.User{Username: user.Username})
if err != nil {
return err
}
if dbUser == nil {
return u.OnBoardUser(user)
}
if user.Email != "" {
dbUser.Email = user.Email
}
if user.Realname != "" {
dbUser.Realname = user.Realname
}
if err2 := dao.ChangeUserProfile(*user, "Email", "Realname"); err2 != nil {
log.Warningf("Failed to update user profile, user: %s, error: %v", user.Username, err2)
}
return nil
}
// SearchUser search user on uaa server, transform it to Harbor's user model // SearchUser search user on uaa server, transform it to Harbor's user model
func (u *Auth) SearchUser(username string) (*models.User, error) { func (u *Auth) SearchUser(username string) (*models.User, error) {
if err := u.ensureClient(); err != nil { if err := u.ensureClient(); err != nil {
@ -116,13 +128,27 @@ func (u *Auth) SearchUser(username string) (*models.User, error) {
} }
func (u *Auth) ensureClient() error { func (u *Auth) ensureClient() error {
if u.client != nil { var cfg *uaa.ClientConfig
return nil UAASettings, err := config.UAASettings()
// log.Debugf("Uaa settings: %+v", UAASettings)
if err != nil {
log.Warningf("Failed to get UAA setting from Admin Server, error: %v", err)
} else {
cfg = &uaa.ClientConfig{
ClientID: UAASettings.ClientID,
ClientSecret: UAASettings.ClientSecret,
Endpoint: UAASettings.Endpoint,
SkipTLSVerify: !UAASettings.VerifyCert,
CARootPath: os.Getenv("UAA_CA_ROOT"),
}
}
if u.client != nil && cfg != nil {
return u.client.UpdateConfig(cfg)
} }
u.Lock() u.Lock()
defer u.Unlock() defer u.Unlock()
if u.client == nil { if u.client == nil {
c, err := CreateClient() c, err := uaa.NewDefaultClient(cfg)
if err != nil { if err != nil {
return err return err
} }

View File

@ -102,11 +102,12 @@ func TestMain(m *testing.M) {
os.Exit(rc) os.Exit(rc)
} }
func TestCreateClient(t *testing.T) { func TestEnsureClient(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
c, err := CreateClient() auth := Auth{client: nil}
err := auth.ensureClient()
assert.Nil(err) assert.Nil(err)
assert.NotNil(c) assert.NotNil(auth.client)
} }
func TestAuthenticate(t *testing.T) { func TestAuthenticate(t *testing.T) {
@ -123,6 +124,7 @@ func TestAuthenticate(t *testing.T) {
u1, err1 := auth.Authenticate(m1) u1, err1 := auth.Authenticate(m1)
assert.Nil(err1) assert.Nil(err1)
assert.NotNil(u1) assert.NotNil(u1)
assert.Equal("fake@fake.com", u1.Email)
m2 := models.AuthModel{ m2 := models.AuthModel{
Principal: "wrong", Principal: "wrong",
Password: "wrong", Password: "wrong",
@ -153,6 +155,30 @@ func TestOnBoardUser(t *testing.T) {
assert.Equal("test", user.Realname) assert.Equal("test", user.Realname)
assert.Equal("test", user.Username) assert.Equal("test", user.Username)
assert.Equal("test@uaa.placeholder", user.Email) assert.Equal("test@uaa.placeholder", user.Email)
err3 := dao.ClearTable(models.UserTable)
assert.Nil(err3)
}
func TestPostAuthenticate(t *testing.T) {
assert := assert.New(t)
auth := Auth{}
um := &models.User{
Username: "test",
}
err := auth.PostAuthenticate(um)
assert.Nil(err)
user, _ := dao.GetUser(models.User{Username: "test"})
assert.Equal("test@uaa.placeholder", user.Email)
um.Email = "newEmail@new.com"
um.Realname = "newName"
err2 := auth.PostAuthenticate(um)
assert.Nil(err2)
user2, _ := dao.GetUser(models.User{Username: "test"})
assert.Equal("newEmail@new.com", user2.Email)
assert.Equal("newName", user2.Realname)
err3 := dao.ClearTable(models.UserTable)
assert.Nil(err3)
} }
func TestSearchUser(t *testing.T) { func TestSearchUser(t *testing.T) {