Search UAA when adding member to a project.

1)Enable UAA client to search UAA by calling '/Users' API.
2)Implement 'SearchUser' in UAA auth helper, register it to auth
package.
This commit is contained in:
Tan Jiang 2017-12-21 20:07:23 +08:00
parent 7c510fa2c8
commit da20e4f11c
17 changed files with 495 additions and 137 deletions

View File

@ -15,17 +15,18 @@
package database package database
import ( import (
"github.com/vmware/harbor/src/common/dao" "fmt"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/adminserver/systemcfg/store" "github.com/vmware/harbor/src/adminserver/systemcfg/store"
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"fmt" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"strconv" "strconv"
) )
const ( const (
name = "database" name = "database"
) )
var ( var (
numKeys = map[string]bool{ numKeys = map[string]bool{
common.EmailPort: true, common.EmailPort: true,
@ -44,8 +45,10 @@ var(
common.EmailSSL: true, common.EmailSSL: true,
common.EmailInsecure: true, common.EmailInsecure: true,
common.LDAPVerifyCert: true, common.LDAPVerifyCert: true,
common.UAAVerifyCert: true,
} }
) )
type cfgStore struct { type cfgStore struct {
name string name string
} }
@ -61,6 +64,7 @@ func NewCfgStore() (store.Driver, error){
name: name, name: name,
}, nil }, nil
} }
// Read configuration from database // Read configuration from database
func (c *cfgStore) Read() (map[string]interface{}, error) { func (c *cfgStore) Read() (map[string]interface{}, error) {
configEntries, error := dao.GetConfigEntries() configEntries, error := dao.GetConfigEntries()
@ -93,6 +97,7 @@ func WrapperConfig (configEntries []*models.ConfigEntry) (map[string]interface{}
} }
return config, nil return config, nil
} }
// Write save configuration to database // Write save configuration to database
func (c *cfgStore) Write(config map[string]interface{}) error { func (c *cfgStore) Write(config map[string]interface{}) error {
configEntries, _ := TranslateConfig(config) configEntries, _ := TranslateConfig(config)

View File

@ -171,7 +171,10 @@ var (
common.UAAEndpoint: "UAA_ENDPOINT", common.UAAEndpoint: "UAA_ENDPOINT",
common.UAAClientID: "UAA_CLIENTID", common.UAAClientID: "UAA_CLIENTID",
common.UAAClientSecret: "UAA_CLIENTSECRET", common.UAAClientSecret: "UAA_CLIENTSECRET",
common.UAAVerifyCert: "UAA_VERIFY_CERT", common.UAAVerifyCert: &parser{
env: "UAA_VERIFY_CERT",
parse: parseStringToBool,
},
} }
) )

View File

@ -18,6 +18,7 @@ package common
const ( const (
DBAuth = "db_auth" DBAuth = "db_auth"
LDAPAuth = "ldap_auth" LDAPAuth = "ldap_auth"
UAAAuth = "uaa_auth"
ProCrtRestrEveryone = "everyone" ProCrtRestrEveryone = "everyone"
ProCrtRestrAdmOnly = "adminonly" ProCrtRestrAdmOnly = "adminonly"
LDAPScopeBase = 1 LDAPScopeBase = 1

View File

@ -116,6 +116,12 @@ func GetOrmer() orm.Ormer {
func ClearTable(table string) error { func ClearTable(table string) error {
o := GetOrmer() o := GetOrmer()
sql := fmt.Sprintf("delete from %s where 1=1", table) sql := fmt.Sprintf("delete from %s where 1=1", table)
if table == models.ProjectTable {
sql = fmt.Sprintf("delete from %s where project_id > 1", table)
}
if table == models.UserTable {
sql = fmt.Sprintf("delete from %s where user_id > 2", table)
}
_, err := o.Raw(sql).Exec() _, err := o.Raw(sql).Exec()
return err return err
} }

View File

@ -19,6 +19,9 @@ import (
"time" "time"
) )
// ProjectTable is the table name for project
const ProjectTable = "project"
// Project holds the details of a project. // Project holds the details of a project.
type Project struct { type Project struct {
ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"` ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"`
@ -174,3 +177,8 @@ type ProjectQueryResult struct {
Total int64 Total int64
Projects []*Project Projects []*Project
} }
//TableName is required by beego orm to map Project to table project
func (p *Project) TableName() string {
return ProjectTable
}

View File

@ -18,6 +18,9 @@ import (
"time" "time"
) )
// UserTable is the name of table in DB that holds the user object
const UserTable = "user"
// User holds the details of a user. // User holds the details of a user.
type User struct { type User struct {
UserID int `orm:"pk;auto;column(user_id)" json:"user_id"` UserID int `orm:"pk;auto;column(user_id)" json:"user_id"`
@ -45,3 +48,8 @@ type UserQuery struct {
Email string Email string
Pagination *Pagination Pagination *Pagination
} }
// TableName ...
func (u *User) TableName() string {
return UserTable
}

View File

@ -19,12 +19,25 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
const (
//TokenURLSuffix ...
TokenURLSuffix = "/oauth/token"
//AuthURLSuffix ...
AuthURLSuffix = "/oauth/authorize"
//UserInfoURLSuffix ...
UserInfoURLSuffix = "/userinfo"
//UsersURLSuffix ...
UsersURLSuffix = "/Users"
) )
// Client provides funcs to interact with UAA. // Client provides funcs to interact with UAA.
@ -33,6 +46,8 @@ type Client interface {
PasswordAuth(username, password string) (*oauth2.Token, error) PasswordAuth(username, password string) (*oauth2.Token, error)
//GetUserInfoByToken send the token to OIDC endpoint to get user info, currently it's also used to validate the token. //GetUserInfoByToken send the token to OIDC endpoint to get user info, currently it's also used to validate the token.
GetUserInfo(token string) (*UserInfo, error) GetUserInfo(token string) (*UserInfo, error)
//SearchUser searches a user based on user name.
SearchUser(name string) ([]*SearchUserEntry, error)
} }
// ClientConfig values to initialize UAA Client // ClientConfig values to initialize UAA Client
@ -56,21 +71,43 @@ type UserInfo struct {
Email string `json:"email"` Email string `json:"email"`
} }
//SearchUserEmailEntry ...
type SearchUserEmailEntry struct {
Value string `json:"value"`
Primary bool `json:"primary"`
}
//SearchUserEntry is the struct of an entry of user within search result.
type SearchUserEntry struct {
ID string `json:"id"`
ExtID string `json:"externalId"`
UserName string `json:"userName"`
Emails []SearchUserEmailEntry `json:"emails"`
Groups []interface{}
}
//SearchUserRes is the struct to parse the result of search user API of UAA
type SearchUserRes struct {
Resources []*SearchUserEntry `json:"resources"`
TotalResults int `json:"totalResults"`
Schemas []string `json:"schemas"`
}
// DefaultClient leverages oauth2 pacakge for oauth features // DefaultClient leverages oauth2 pacakge for oauth features
type defaultClient struct { type defaultClient struct {
httpClient *http.Client httpClient *http.Client
oauth2Cfg *oauth2.Config oauth2Cfg *oauth2.Config
twoLegCfg *clientcredentials.Config
endpoint string endpoint string
//TODO: add public key, etc... //TODO: add public key, etc...
} }
func (dc *defaultClient) PasswordAuth(username, password string) (*oauth2.Token, error) { func (dc *defaultClient) PasswordAuth(username, password string) (*oauth2.Token, error) {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient) return dc.oauth2Cfg.PasswordCredentialsToken(dc.prepareCtx(), username, password)
return dc.oauth2Cfg.PasswordCredentialsToken(ctx, username, password)
} }
func (dc *defaultClient) GetUserInfo(token string) (*UserInfo, error) { func (dc *defaultClient) GetUserInfo(token string) (*UserInfo, error) {
userInfoURL := dc.endpoint + "/uaa/userinfo" userInfoURL := dc.endpoint + UserInfoURLSuffix
req, err := http.NewRequest(http.MethodGet, userInfoURL, nil) req, err := http.NewRequest(http.MethodGet, userInfoURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -92,6 +129,45 @@ func (dc *defaultClient) GetUserInfo(token string) (*UserInfo, error) {
return info, nil return info, nil
} }
func (dc *defaultClient) SearchUser(username string) ([]*SearchUserEntry, error) {
token, err := dc.twoLegCfg.Token(dc.prepareCtx())
if err != nil {
return nil, err
}
url := dc.endpoint + UsersURLSuffix
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("filter", fmt.Sprintf("Username eq '%s'", username))
req.URL.RawQuery = q.Encode()
token.SetAuthHeader(req)
log.Debugf("request URL: %s", req.URL)
resp, err := dc.httpClient.Do(req)
if err != nil {
return nil, err
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unexpected status code for searching user in UAA: %d, response: %s", resp.StatusCode, string(bytes))
}
res := &SearchUserRes{}
if err := json.Unmarshal(bytes, res); err != nil {
return nil, err
}
return res.Resources, nil
}
func (dc *defaultClient) prepareCtx() context.Context {
return context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient)
}
// NewDefaultClient creates an instance of defaultClient. // NewDefaultClient creates an instance of defaultClient.
func NewDefaultClient(cfg *ClientConfig) (Client, error) { func NewDefaultClient(cfg *ClientConfig) (Client, error) {
url := cfg.Endpoint url := cfg.Endpoint
@ -125,14 +201,21 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
ClientID: cfg.ClientID, ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret, ClientSecret: cfg.ClientSecret,
Endpoint: oauth2.Endpoint{ Endpoint: oauth2.Endpoint{
TokenURL: url + "/uaa/oauth/token", TokenURL: url + TokenURLSuffix,
AuthURL: url + "/uaa/oauth/authorize", AuthURL: url + AuthURLSuffix,
}, },
} }
cc := &clientcredentials.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
TokenURL: url + TokenURLSuffix,
}
return &defaultClient{ return &defaultClient{
httpClient: hc, httpClient: hc,
oauth2Cfg: oc, oauth2Cfg: oc,
twoLegCfg: cc,
endpoint: url, endpoint: url,
}, nil }, nil
} }

View File

@ -30,15 +30,18 @@ func TestMain(m *testing.M) {
} }
} }
func TestPasswordAuth(t *testing.T) { func getCfg() *ClientConfig {
cfg := &ClientConfig{ return &ClientConfig{
ClientID: "uaa", ClientID: "uaa",
ClientSecret: "secret", ClientSecret: "secret",
Endpoint: mockUAAServer.URL, Endpoint: mockUAAServer.URL,
SkipTLSVerify: true, SkipTLSVerify: true,
} }
}
func TestPasswordAuth(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
client, err := NewDefaultClient(cfg) client, err := NewDefaultClient(getCfg())
assert.Nil(err) assert.Nil(err)
_, err = client.PasswordAuth("user1", "pass1") _, err = client.PasswordAuth("user1", "pass1")
assert.Nil(err) assert.Nil(err)
@ -47,14 +50,8 @@ func TestPasswordAuth(t *testing.T) {
} }
func TestUserInfo(t *testing.T) { func TestUserInfo(t *testing.T) {
cfg := &ClientConfig{
ClientID: "uaa",
ClientSecret: "secret",
Endpoint: mockUAAServer.URL,
SkipTLSVerify: true,
}
assert := assert.New(t) assert := assert.New(t)
client, err := NewDefaultClient(cfg) client, err := NewDefaultClient(getCfg())
assert.Nil(err) assert.Nil(err)
token, err := ioutil.ReadFile(path.Join(currPath(), "test", "./good-access-token.txt")) token, err := ioutil.ReadFile(path.Join(currPath(), "test", "./good-access-token.txt"))
if err != nil { if err != nil {
@ -68,6 +65,21 @@ func TestUserInfo(t *testing.T) {
assert.NotNil(err2) assert.NotNil(err2)
} }
func TestSearchUser(t *testing.T) {
assert := assert.New(t)
client, err := NewDefaultClient(getCfg())
assert.Nil(err)
res1, err := client.SearchUser("one")
assert.Nil(err)
assert.Equal(1, len(res1))
if len(res1) == 1 {
assert.Equal("one", res1[0].UserName)
}
res2, err := client.SearchUser("none")
assert.Nil(err)
assert.Equal(0, len(res2))
}
func currPath() string { func currPath() string {
_, f, _, ok := runtime.Caller(0) _, f, _, ok := runtime.Caller(0)
if !ok { if !ok {

View File

@ -37,3 +37,40 @@ func (fc *FakeClient) PasswordAuth(username, password string) (*oauth2.Token, er
func (fc *FakeClient) GetUserInfo(token string) (*UserInfo, error) { func (fc *FakeClient) GetUserInfo(token string) (*UserInfo, error) {
return nil, nil return nil, nil
} }
// SearchUser ...
func (fc *FakeClient) SearchUser(name string) ([]*SearchUserEntry, error) {
res := []*SearchUserEntry{}
entryOne := &SearchUserEntry{
ExtID: "some-external-id-1",
ID: "u-0001",
UserName: "one",
Emails: []SearchUserEmailEntry{SearchUserEmailEntry{
Primary: false,
Value: "one@email.com",
}},
}
entryTwoA := &SearchUserEntry{
ExtID: "some-external-id-2-a",
ID: "u-0002a",
UserName: "two",
Emails: []SearchUserEmailEntry{SearchUserEmailEntry{
Primary: false,
Value: "two@email.com",
}},
}
entryTwoB := &SearchUserEntry{
ExtID: "some-external-id-2-b",
ID: "u-0002b",
UserName: "two",
}
if name == "one" {
res = append(res, entryOne)
} else if name == "two" {
res = append(res, entryTwoA)
res = append(res, entryTwoB)
} else if name == "error" {
return res, fmt.Errorf("some error")
}
return res, nil
}

View File

@ -1 +1 @@
eyJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNmRjYjg1YzMzZjU0OGM5ODk2YjI4MDEwN2IyOWM0NiIsInN1YiI6IjlhMTM0ODhmLWYzY2YtNDdhNi05OGYwLTRmZWQyMWY0MzUyMCIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiOWExMzQ4OGYtZjNjZi00N2E2LTk4ZjAtNGZlZDIxZjQzNTIwIiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6InVzZXIwMSIsImVtYWlsIjoidXNlcjAxQHVzZXIuZnJvbS5sZGFwLmNmIiwiYXV0aF90aW1lIjoxNTExNDA1NDEwLCJyZXZfc2lnIjoiOGEwYmY5OWQiLCJpYXQiOjE1MTE0MDU0MTAsImV4cCI6MTUxMTQ0ODYxMCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0My91YWEvb2F1dGgvdG9rZW4iLCJ6aWQiOiJ1YWEiLCJhdWQiOlsia3ViZXJuZXRlcyIsIm9wZW5pZCJdfQ.I7VBx_cQoYkotRJ8KdmESAf_xjzp-R44BRz9ngHPUnoqr4rSMin-Ful8wNzEnaYaG56_mrIPuLOb6vXGWW1svRU892GOK9WQRSiFp7O81V7f1bH6JXnIGvyBNl3JOkDB9d5wXn137h9vNKq3Z9TF3jD7oXR_OENS8paclW5EAjmjGvEVIhObMmHCLhsJshTWIoP8AwoP1m9iqak_-t0c99HWaf1AgVUtT2i9Jb63ndJGA6BkOSRH_YxXmM_qtXmk_0kRA5oLDR2UGA4TVXCYp1_8iwQYjvGBVxO24I5jJh_zDYs5YLTFeNzMTPEhAl_Te6NiE91gRXq6KiVk9tTfuA eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNzlhNmI2MTRhMzM0NjVjYjYxZTM4ZmY5YTc4Y2YxZSIsInN1YiI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbkB1c2VyLmZyb20ubGRhcC5jZiIsImF1dGhfdGltZSI6MTUwNjg1MDQyNiwicmV2X3NpZyI6IjZkOWNlN2UwIiwiaWF0IjoxNTA2ODUwNDI2LCJleHAiOjE1MDY4OTM2MjYsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbImt1YmVybmV0ZXMiLCJvcGVuaWQiXX0.Ni2yJ7Gp6OnEdhcWyfGeCm1yG_rqQgf9BA0raJ37hdH-_ZRQ4HIELkWLv3gGPuWPV4HX6EKKerjJWXCPKihyiIIVT-W7VkwFMdDv9e4aA_h2eXjxHeUdjl0Cgw7gSAPSmm_QtkeLPuj15Ngd31yiuBoxy49_sjCyn3hjd8LP2ENEVtpk2vcCiQigW-YWbDaG64im1IP6jjRruwRdPF0Idjf4vuimFG-tiRdauDvnZc90W5fIJ3AUFW_ryGnSvc7E0rBZFYOgD5BB_3HLmWzB64-D3AVe9h5wQXOBorEaXLlSXfm16RQHFI_duSh3YOZUjHLuUYIRCuKaK5RPi0Fztg

View File

@ -0,0 +1 @@
{"resources":[],"startIndex":1,"itemsPerPage":100,"totalResults":0,"schemas":["urn:scim:schemas:core:1.0"]}

View File

@ -0,0 +1 @@
{"resources":[{"id":"6af888a1-92fa-4a30-82dd-4db28f2e15f0","externalId":"cn=one,dc=vmware,dc=com","meta":{"version":0,"created":"2017-12-20T22:54:34.493Z","lastModified":"2017-12-20T22:54:34.493Z"},"userName":"one","name":{},"emails":[{"value":"one@example.com","primary":false}],"groups":[{"value":"546a79d3-609b-49df-8111-56eee574fc99","display":"roles","type":"DIRECT"},{"value":"7e5eb7dc-8067-424a-a593-8620e9ef4962","display":"approvals.me","type":"DIRECT"},{"value":"9a946687-7be7-4a79-9742-462ae52e4833","display":"password.write","type":"DIRECT"},{"value":"f263a309-f855-405b-bcc4-e1c7453420c3","display":"uaa.offline_token","type":"DIRECT"},{"value":"80898c93-64a8-46cc-ba15-37fec9e2e56d","display":"uaa.user","type":"DIRECT"},{"value":"f8605b49-0dbc-47cf-a993-9691b7e313ab","display":"scim.userids","type":"DIRECT"},{"value":"83237f80-e709-40b9-8599-ab08b0f141a9","display":"oauth.approvals","type":"DIRECT"},{"value":"f685504d-c760-41cf-9c26-da8edddf643e","display":"user_attributes","type":"DIRECT"},{"value":"4243ba6a-001f-4052-8059-ada841a14e62","display":"cloud_controller.write","type":"DIRECT"},{"value":"36a94fb1-3bd2-4db6-8246-8a00256b080f","display":"profile","type":"DIRECT"},{"value":"0ead714c-02f1-403b-bd24-950089772f47","display":"scim.me","type":"DIRECT"},{"value":"a0944e04-1007-43ba-9745-e1ed62de21f5","display":"cloud_controller.read","type":"DIRECT"},{"value":"e9f2b839-9e2d-45b4-9179-5fce07cd013b","display":"cloud_controller_service_permissions.read","type":"DIRECT"},{"value":"2bb835d6-c62d-477b-a1df-780bb3ec560b","display":"openid","type":"DIRECT"}],"approvals":[],"active":true,"verified":true,"origin":"ldap","zoneId":"uaa","passwordLastModified":"2017-12-20T22:54:34.000Z","lastLogonTime":1513839274546,"schemas":["urn:scim:schemas:core:1.0"]}],"startIndex":1,"itemsPerPage":100,"totalResults":1,"schemas":["urn:scim:schemas:core:1.0"]}

View File

@ -53,24 +53,37 @@ func (t *tokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
http.Error(rw, "invalid client id/secret in header", http.StatusUnauthorized) http.Error(rw, "invalid client id/secret in header", http.StatusUnauthorized)
return return
} }
if gt := req.FormValue("grant_type"); gt != "password" { gt := req.FormValue("grant_type")
http.Error(rw, fmt.Sprintf("invalid grant_type: %s", gt), http.StatusBadRequest) if gt == "password" {
return
}
reqUsername := req.FormValue("username") reqUsername := req.FormValue("username")
reqPasswd := req.FormValue("password") reqPasswd := req.FormValue("password")
if reqUsername == t.username && reqPasswd == t.password { if reqUsername == t.username && reqPasswd == t.password {
token, err := ioutil.ReadFile(path.Join(currPath(), "./uaa-token.json")) serveToken(rw)
} else {
http.Error(rw, fmt.Sprintf("invalid username/password %s/%s", reqUsername, reqPasswd), http.StatusUnauthorized)
}
} else if gt == "client_credentials" {
serveToken(rw)
} else {
http.Error(rw, fmt.Sprintf("invalid grant_type: %s", gt), http.StatusBadRequest)
return
}
}
func serveToken(rw http.ResponseWriter) {
serveJSONFile(rw, "uaa-token.json")
}
func serveJSONFile(rw http.ResponseWriter, filename string) {
data, err := ioutil.ReadFile(path.Join(currPath(), filename))
if err != nil { if err != nil {
panic(err) panic(err)
} }
_, err2 := rw.Write(token) rw.Header().Add("Content-Type", "application/json")
_, err2 := rw.Write(data)
if err2 != nil { if err2 != nil {
panic(err2) panic(err2)
} }
} else {
http.Error(rw, fmt.Sprintf("invalid username/password %s/%s", reqUsername, reqPasswd), http.StatusUnauthorized)
}
} }
type userInfoHandler struct { type userInfoHandler struct {
@ -78,27 +91,52 @@ type userInfoHandler struct {
} }
func (u *userInfoHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (u *userInfoHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
v := req.Header.Get("Authorization") v := req.Header.Get("authorization")
prefix := v[0:7] prefix := v[0:7]
reqToken := v[7:] reqToken := v[7:]
if strings.ToLower(prefix) != "bearer " || reqToken != u.token { if strings.ToLower(prefix) != "bearer " || reqToken != u.token {
http.Error(rw, "invalid token", http.StatusUnauthorized) http.Error(rw, "invalid token", http.StatusUnauthorized)
return return
} }
userInfo, err := ioutil.ReadFile(path.Join(currPath(), "./user-info.json")) serveJSONFile(rw, "./user-info.json")
if err != nil {
panic(err)
} }
_, err2 := rw.Write(userInfo)
if err2 != nil { type searchUserHandler struct {
panic(err2) token string
} }
func (su *searchUserHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
v := req.Header.Get("authorization")
if v == "" {
v = req.Header.Get("Authorization")
}
prefix := v[0:7]
reqToken := v[7:]
if strings.ToLower(prefix) != "bearer " || reqToken != su.token {
http.Error(rw, "invalid token", http.StatusUnauthorized)
return
}
f := req.URL.Query().Get("filter")
elements := strings.Split(f, " ")
if len(elements) == 3 {
if elements[0] == "Username" && elements[1] == "eq" {
if elements[2] == "'one'" {
serveJSONFile(rw, "one-user.json")
return
}
serveJSONFile(rw, "no-user.json")
return
}
http.Error(rw, "invalid request", http.StatusBadRequest)
return
}
http.Error(rw, fmt.Sprintf("Invalid request, elements: %v", elements), http.StatusBadRequest)
} }
// NewMockServer ... // NewMockServer ...
func NewMockServer(cfg *MockServerConfig) *httptest.Server { func NewMockServer(cfg *MockServerConfig) *httptest.Server {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/uaa/oauth/token", &tokenHandler{ mux.Handle("/oauth/token", &tokenHandler{
cfg.ClientID, cfg.ClientID,
cfg.ClientSecret, cfg.ClientSecret,
cfg.Username, cfg.Username,
@ -108,6 +146,7 @@ func NewMockServer(cfg *MockServerConfig) *httptest.Server {
if err != nil { if err != nil {
panic(err) panic(err)
} }
mux.Handle("/uaa/userinfo", &userInfoHandler{strings.TrimSpace(string(token))}) mux.Handle("/userinfo", &userInfoHandler{strings.TrimSpace(string(token))})
mux.Handle("/Users", &searchUserHandler{strings.TrimSpace(string(token))})
return httptest.NewTLSServer(mux) return httptest.NewTLSServer(mux)
} }

View File

@ -1 +1 @@
{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNzlhNmI2MTRhMzM0NjVjYjYxZTM4ZmY5YTc4Y2YxZSIsInN1YiI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbkB1c2VyLmZyb20ubGRhcC5jZiIsImF1dGhfdGltZSI6MTUwNjg1MDQyNiwicmV2X3NpZyI6IjZkOWNlN2UwIiwiaWF0IjoxNTA2ODUwNDI2LCJleHAiOjE1MDY4OTM2MjYsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbImt1YmVybmV0ZXMiLCJvcGVuaWQiXX0.Ni2yJ7Gp6OnEdhcWyfGeCm1yG_rqQgf9BA0raJ37hdH-_ZRQ4HIELkWLv3gGPuWPV4HX6EKKerjJWXCPKihyiIIVT-W7VkwFMdDv9e4aA_h2eXjxHeUdjl0Cgw7gSAPSmm_QtkeLPuj15Ngd31yiuBoxy49_sjCyn3hjd8LP2ENEVtpk2vcCiQigW-YWbDaG64im1IP6jjRruwRdPF0Idjf4vuimFG-tiRdauDvnZc90W5fIJ3AUFW_ryGnSvc7E0rBZFYOgD5BB_3HLmWzB64-D3AVe9h5wQXOBorEaXLlSXfm16RQHFI_duSh3YOZUjHLuUYIRCuKaK5RPi0Fztg","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJkZTU0ZjJkMDlkODc0ZTliOTExZDk4YWQ1MTQzMjljZC1yIiwic3ViIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwic2NvcGUiOlsib3BlbmlkIl0sImlhdCI6MTUwNjg1MDQyNiwiZXhwIjoxNTA5NDQyNDI2LCJjaWQiOiJrdWJlcm5ldGVzIiwiY2xpZW50X2lkIjoia3ViZXJuZXRlcyIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiZ3JhbnRfdHlwZSI6InBhc3N3b3JkIiwidXNlcl9uYW1lIjoiYWRtaW4iLCJvcmlnaW4iOiJsZGFwIiwidXNlcl9pZCI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInJldl9zaWciOiI2ZDljZTdlMCIsImF1ZCI6WyJrdWJlcm5ldGVzIiwib3BlbmlkIl19.oW4xK3QBjMtjUH_AWWyO6A0QwbIbTwrEFnc-hulj3QbLoULvC2V3L53rcKhT1gOtj8aaQTZFdBEQNGjBpzjFU8bpwxb0szyPMkc5PjXjcJGltL3MvmBf3P0TuUxJU9vP3FjrvwwueNAafLAyRIHy8yA3ZngzkL8KCI0ps51gCRU2oOe9hGDv2ZrsZ21u760hFGiRq5-7HWJu3VMqhMVRkUyPD_3j9AGZr6gf3o_7S9oJYwEDxPZaBhhVZI6QHeQNa07w7jCqTX97_fcpeTMbrBJiz_5yD9-kJZneI4xzAMIyNwAcbSJYrL7WZ2H01heGwWFEkrrv68YUJ762jB4WAw","expires_in":43199,"scope":"openid","jti":"279a6b614a33465cb61e38ff9a78cf1e"} {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNzlhNmI2MTRhMzM0NjVjYjYxZTM4ZmY5YTc4Y2YxZSIsInN1YiI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbkB1c2VyLmZyb20ubGRhcC5jZiIsImF1dGhfdGltZSI6MTUwNjg1MDQyNiwicmV2X3NpZyI6IjZkOWNlN2UwIiwiaWF0IjoxNTA2ODUwNDI2LCJleHAiOjE1MDY4OTM2MjYsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbImt1YmVybmV0ZXMiLCJvcGVuaWQiXX0.Ni2yJ7Gp6OnEdhcWyfGeCm1yG_rqQgf9BA0raJ37hdH-_ZRQ4HIELkWLv3gGPuWPV4HX6EKKerjJWXCPKihyiIIVT-W7VkwFMdDv9e4aA_h2eXjxHeUdjl0Cgw7gSAPSmm_QtkeLPuj15Ngd31yiuBoxy49_sjCyn3hjd8LP2ENEVtpk2vcCiQigW-YWbDaG64im1IP6jjRruwRdPF0Idjf4vuimFG-tiRdauDvnZc90W5fIJ3AUFW_ryGnSvc7E0rBZFYOgD5BB_3HLmWzB64-D3AVe9h5wQXOBorEaXLlSXfm16RQHFI_duSh3YOZUjHLuUYIRCuKaK5RPi0Fztg","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJkZTU0ZjJkMDlkODc0ZTliOTExZDk4YWQ1MTQzMjljZC1yIiwic3ViIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwic2NvcGUiOlsib3BlbmlkIl0sImlhdCI6MTUwNjg1MDQyNiwiZXhwIjoxNTA5NDQyNDI2LCJjaWQiOiJrdWJlcm5ldGVzIiwiY2xpZW50X2lkIjoia3ViZXJuZXRlcyIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiZ3JhbnRfdHlwZSI6InBhc3N3b3JkIiwidXNlcl9uYW1lIjoiYWRtaW4iLCJvcmlnaW4iOiJsZGFwIiwidXNlcl9pZCI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInJldl9zaWciOiI2ZDljZTdlMCIsImF1ZCI6WyJrdWJlcm5ldGVzIiwib3BlbmlkIl19.oW4xK3QBjMtjUH_AWWyO6A0QwbIbTwrEFnc-hulj3QbLoULvC2V3L53rcKhT1gOtj8aaQTZFdBEQNGjBpzjFU8bpwxb0szyPMkc5PjXjcJGltL3MvmBf3P0TuUxJU9vP3FjrvwwueNAafLAyRIHy8yA3ZngzkL8KCI0ps51gCRU2oOe9hGDv2ZrsZ21u760hFGiRq5-7HWJu3VMqhMVRkUyPD_3j9AGZr6gf3o_7S9oJYwEDxPZaBhhVZI6QHeQNa07w7jCqTX97_fcpeTMbrBJiz_5yD9-kJZneI4xzAMIyNwAcbSJYrL7WZ2H01heGwWFEkrrv68YUJ762jB4WAw","expires_in":43199,"jti":"279a6b614a33465cb61e38ff9a78cf1e", "scope":"clients.read password.write clients.secret uaa.resource openid clients.write uaa.admin scim.write scim.read client_id"}

View File

@ -46,10 +46,11 @@ var registry = make(map[string]AuthenticateHelper)
// Register add different authenticators to registry map. // Register add different authenticators to registry map.
func Register(name string, h AuthenticateHelper) { func Register(name string, h AuthenticateHelper) {
if _, dup := registry[name]; dup { if _, dup := registry[name]; dup {
log.Infof("authenticator: %s has been registered", name) log.Infof("authenticator: %s has been registered,skip", name)
return return
} }
registry[name] = h registry[name] = h
log.Debugf("Registered authencation helper for auth mode: %s", name)
} }
// Login authenticates user credentials based on setting. // Login authenticates user credentials based on setting.

View File

@ -15,24 +15,20 @@
package uaa package uaa
import ( import (
"fmt"
"strings"
"sync" "sync"
"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/uaa" "github.com/vmware/harbor/src/common/utils/uaa"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
) )
var lock = &sync.Mutex{} //CreateClient create a UAA Client instance based on system configuration.
var client uaa.Client func CreateClient() (uaa.Client, error) {
//GetClient returns the client instance, if the client is not created it creates one.
func GetClient() (uaa.Client, error) {
lock.Lock()
defer lock.Unlock()
if client != nil {
return client, nil
}
UAASettings, err := config.UAASettings() UAASettings, err := config.UAASettings()
if err != nil { if err != nil {
return nil, err return nil, err
@ -43,51 +39,94 @@ func GetClient() (uaa.Client, error) {
Endpoint: UAASettings.Endpoint, Endpoint: UAASettings.Endpoint,
SkipTLSVerify: !UAASettings.VerifyCert, SkipTLSVerify: !UAASettings.VerifyCert,
} }
client, err = uaa.NewDefaultClient(cfg) return uaa.NewDefaultClient(cfg)
return client, err
}
func doAuth(username, password string, client uaa.Client) (*models.User, error) {
t, err := client.PasswordAuth(username, password)
if t != nil && err == nil {
//TODO: See if it's possible to get more information from token.
u := &models.User{
Username: username,
Password: "1234567ab",
Email: username + "@placeholder.com",
Realname: username,
}
err = dao.OnBoardUser(u)
if err == nil {
return u, nil
}
}
return nil, err
} }
// 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
client uaa.Client
}
//Authenticate ... //Authenticate ...
func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) { func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
client, err := GetClient() if err := u.ensureClient(); err != nil {
if err != nil {
return nil, err return nil, err
} }
return doAuth(m.Principal, m.Password, client) t, err := u.client.PasswordAuth(m.Principal, m.Password)
if t != nil && err == nil {
//TODO: See if it's possible to get more information from token.
user := &models.User{
Username: m.Principal,
}
err = u.OnBoardUser(user)
return user, err
}
return nil, err
} }
// OnBoardUser will check if a user exists in user table, if not insert the user and // OnBoardUser will check if a user exists in user table, if not insert the user and
// put the id in the pointer of user model, if it does exist, return the user's profile. // put the id in the pointer of user model, if it does exist, return the user's profile.
// func (u *Auth) OnBoardUser(user *models.User) error { func (u *Auth) OnBoardUser(user *models.User) error {
// panic("not implemented") user.Username = strings.TrimSpace(user.Username)
// } if len(user.Username) == 0 {
return fmt.Errorf("The Username is empty")
}
if len(user.Password) == 0 {
user.Password = "1234567ab"
}
if len(user.Realname) == 0 {
user.Realname = user.Username
}
if len(user.Email) == 0 {
//TODO: handle the case when user.Username itself is an email address.
user.Email = user.Username + "@uaa.placeholder"
}
user.Comment = "From UAA"
return dao.OnBoardUser(user)
}
// // SearchUser - search user on uaa server // 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) {
// panic("not implemented") if err := u.ensureClient(); err != nil {
// } return nil, err
}
l, err := u.client.SearchUser(username)
if err != nil {
return nil, err
}
if len(l) == 0 {
return nil, nil
}
if len(l) > 1 {
return nil, fmt.Errorf("Multiple entries found for username: %s", username)
}
e := l[0]
email := ""
if len(e.Emails) > 0 {
email = e.Emails[0].Value
}
return &models.User{
Username: username,
Email: email,
}, nil
}
// func init() { func (u *Auth) ensureClient() error {
// auth.Register(auth.UAAAuth, &Auth{}) if u.client != nil {
// } return nil
}
u.Lock()
defer u.Unlock()
if u.client == nil {
c, err := CreateClient()
if err != nil {
return err
}
u.client = c
}
return nil
}
func init() {
auth.Register(common.UAAAuth, &Auth{})
}

View File

@ -17,45 +17,159 @@ package uaa
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"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/utils/log"
utilstest "github.com/vmware/harbor/src/common/utils/test" utilstest "github.com/vmware/harbor/src/common/utils/test"
"github.com/vmware/harbor/src/common/utils/uaa" "github.com/vmware/harbor/src/common/utils/uaa"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
"os" "os"
"strconv"
"testing" "testing"
) )
func TestGetClient(t *testing.T) { func TestMain(m *testing.M) {
assert := assert.New(t) dbHost := os.Getenv("MYSQL_HOST")
if len(dbHost) == 0 {
log.Fatalf("environment variable MYSQL_HOST is not set")
}
dbUser := os.Getenv("MYSQL_USR")
if len(dbUser) == 0 {
log.Fatalf("environment variable MYSQL_USR is not set")
}
dbPortStr := os.Getenv("MYSQL_PORT")
if len(dbPortStr) == 0 {
log.Fatalf("environment variable MYSQL_PORT is not set")
}
dbPort, err := strconv.Atoi(dbPortStr)
if err != nil {
log.Fatalf("invalid MYSQL_PORT: %v", err)
}
dbPassword := os.Getenv("MYSQL_PWD")
dbDatabase := os.Getenv("MYSQL_DATABASE")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable MYSQL_DATABASE is not set")
}
database := &models.Database{
Type: "mysql",
MySQL: &models.MySQL{
Host: dbHost,
Port: dbPort,
Username: dbUser,
Password: dbPassword,
Database: dbDatabase,
},
}
dao.InitDatabase(database)
server, err := utilstest.NewAdminserver(nil) server, err := utilstest.NewAdminserver(nil)
if err != nil { if err != nil {
t.Fatalf("failed to create a mock admin server: %v", err) panic(err)
} }
defer server.Close() defer server.Close()
if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil { if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil {
t.Fatalf("failed to set env %s: %v", "ADMINSERVER_URL", err) panic(err)
} }
err = config.Init() err = config.Init()
if err != nil { if err != nil {
t.Fatalf("failed to init config: %v", err) panic(err)
} }
c, err := GetClient()
err = dao.ClearTable("project_member")
if err != nil {
panic(err)
}
err = dao.ClearTable("project_metadata")
if err != nil {
panic(err)
}
err = dao.ClearTable("access_log")
if err != nil {
panic(err)
}
err = dao.ClearTable("project")
if err != nil {
panic(err)
}
err = dao.ClearTable("user")
if err != nil {
panic(err)
}
rc := m.Run()
os.Exit(rc)
}
func TestCreateClient(t *testing.T) {
assert := assert.New(t)
c, err := CreateClient()
assert.Nil(err) assert.Nil(err)
assert.NotNil(c) assert.NotNil(c)
} }
func TestDoAuth(t *testing.T) { func TestAuthenticate(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
client := &uaa.FakeClient{ client := &uaa.FakeClient{
Username: "user1", Username: "user1",
Password: "password1", Password: "password1",
} }
dao.PrepareTestForMySQL() auth := Auth{client: client}
u1, err1 := doAuth("user1", "password1", client) m1 := models.AuthModel{
Principal: "user1",
Password: "password1",
}
u1, err1 := auth.Authenticate(m1)
assert.Nil(err1) assert.Nil(err1)
assert.True(u1.UserID > 0) assert.NotNil(u1)
u2, err2 := doAuth("wrong", "wrong", client) m2 := models.AuthModel{
Principal: "wrong",
Password: "wrong",
}
u2, err2 := auth.Authenticate(m2)
assert.NotNil(err2) assert.NotNil(err2)
assert.Nil(u2) assert.Nil(u2)
err3 := dao.ClearTable(models.UserTable)
assert.Nil(err3)
}
func TestOnBoardUser(t *testing.T) {
assert := assert.New(t)
auth := Auth{}
um1 := &models.User{
Username: " ",
}
err1 := auth.OnBoardUser(um1)
assert.NotNil(err1)
um2 := &models.User{
Username: "test ",
}
user2, _ := dao.GetUser(models.User{Username: "test"})
assert.Nil(user2)
err2 := auth.OnBoardUser(um2)
assert.Nil(err2)
user, _ := dao.GetUser(models.User{Username: "test"})
assert.Equal("test", user.Realname)
assert.Equal("test", user.Username)
assert.Equal("test@uaa.placeholder", user.Email)
}
func TestSearchUser(t *testing.T) {
assert := assert.New(t)
client := &uaa.FakeClient{
Username: "user1",
Password: "password1",
}
auth := Auth{client: client}
_, err0 := auth.SearchUser("error")
assert.NotNil(err0)
u1, err1 := auth.SearchUser("one")
assert.Nil(err1)
assert.Equal("one@email.com", u1.Email)
_, err2 := auth.SearchUser("two")
assert.NotNil(err2)
user3, err3 := auth.SearchUser("none")
assert.Nil(user3)
assert.Nil(err3)
} }