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,37 +15,40 @@
package database
import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"fmt"
"github.com/vmware/harbor/src/adminserver/systemcfg/store"
"github.com/vmware/harbor/src/common"
"fmt"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"strconv"
)
const (
name = "database"
)
var(
)
var (
numKeys = map[string]bool{
common.EmailPort:true,
common.LDAPScope:true,
common.LDAPTimeout:true,
common.TokenExpiration:true,
common.MySQLPort:true,
common.MaxJobWorkers:true,
common.CfgExpiration:true,
common.ClairDBPort:true,
}
boolKeys = map[string]bool{
common.WithClair:true,
common.WithNotary:true,
common.SelfRegistration:true,
common.EmailSSL:true,
common.EmailInsecure:true,
common.LDAPVerifyCert:true,
common.EmailPort: true,
common.LDAPScope: true,
common.LDAPTimeout: true,
common.TokenExpiration: true,
common.MySQLPort: true,
common.MaxJobWorkers: true,
common.CfgExpiration: true,
common.ClairDBPort: true,
}
)
boolKeys = map[string]bool{
common.WithClair: true,
common.WithNotary: true,
common.SelfRegistration: true,
common.EmailSSL: true,
common.EmailInsecure: true,
common.LDAPVerifyCert: true,
common.UAAVerifyCert: true,
}
)
type cfgStore struct {
name string
}
@ -56,14 +59,15 @@ func (c *cfgStore) Name() string {
}
// NewCfgStore New a cfg store for database driver
func NewCfgStore() (store.Driver, error){
func NewCfgStore() (store.Driver, error) {
return &cfgStore{
name: name,
}, nil
}
// Read configuration from database
func (c *cfgStore) Read() (map[string]interface{}, error) {
configEntries,error := dao.GetConfigEntries()
configEntries, error := dao.GetConfigEntries()
if error != nil {
return nil, error
}
@ -71,53 +75,54 @@ func (c *cfgStore) Read() (map[string]interface{}, error) {
}
// WrapperConfig Wrapper the configuration
func WrapperConfig (configEntries []*models.ConfigEntry) (map[string]interface{}, error) {
func WrapperConfig(configEntries []*models.ConfigEntry) (map[string]interface{}, error) {
config := make(map[string]interface{})
for _,entry := range configEntries{
if numKeys[entry.Key]{
for _, entry := range configEntries {
if numKeys[entry.Key] {
strvalue, err := strconv.Atoi(entry.Value)
if err != nil {
return nil, err
}
config[entry.Key] = float64(strvalue)
}else if boolKeys[entry.Key] {
} else if boolKeys[entry.Key] {
strvalue, err := strconv.ParseBool(entry.Value)
if err != nil {
return nil, err
}
config[entry.Key]=strvalue
}else{
config[entry.Key] = strvalue
} else {
config[entry.Key] = entry.Value
}
}
return config, nil
}
// Write save configuration to database
func (c *cfgStore) Write(config map[string]interface{}) error {
configEntries ,_:= TranslateConfig(config)
configEntries, _ := TranslateConfig(config)
return dao.SaveConfigEntries(configEntries)
}
// TranslateConfig Translate configuration from int, bool, float64 to string
func TranslateConfig(config map[string]interface{}) ([]models.ConfigEntry,error) {
func TranslateConfig(config map[string]interface{}) ([]models.ConfigEntry, error) {
var configEntries []models.ConfigEntry
for k, v := range config {
var entry = new(models.ConfigEntry)
entry.Key = k
switch v.(type) {
case string:
entry.Value=v.(string)
entry.Value = v.(string)
case int:
entry.Value=strconv.Itoa(v.(int))
entry.Value = strconv.Itoa(v.(int))
case bool:
entry.Value=strconv.FormatBool(v.(bool))
entry.Value = strconv.FormatBool(v.(bool))
case float64:
entry.Value=strconv.Itoa(int(v.(float64)))
entry.Value = strconv.Itoa(int(v.(float64)))
default:
return nil, fmt.Errorf("unknown type %v", v)
}
configEntries = append(configEntries,*entry)
configEntries = append(configEntries, *entry)
}
return configEntries,nil
return configEntries, nil
}

View File

@ -130,10 +130,10 @@ var (
parse: parseStringToBool,
},
common.ClairDBPassword: "CLAIR_DB_PASSWORD",
common.ClairDB: "CLAIR_DB",
common.ClairDB: "CLAIR_DB",
common.ClairDBUsername: "CLAIR_DB_USERNAME",
common.ClairDBHost: "CLAIR_DB_HOST",
common.ClairDBPort: "CLAIR_DB_PORT",
common.ClairDBHost: "CLAIR_DB_HOST",
common.ClairDBPort: "CLAIR_DB_PORT",
common.UAAEndpoint: "UAA_ENDPOINT",
common.UAAClientID: "UAA_CLIENTID",
common.UAAClientSecret: "UAA_CLIENTSECRET",
@ -171,7 +171,10 @@ var (
common.UAAEndpoint: "UAA_ENDPOINT",
common.UAAClientID: "UAA_CLIENTID",
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 (
DBAuth = "db_auth"
LDAPAuth = "ldap_auth"
UAAAuth = "uaa_auth"
ProCrtRestrEveryone = "everyone"
ProCrtRestrAdmOnly = "adminonly"
LDAPScopeBase = 1

View File

@ -116,6 +116,12 @@ func GetOrmer() orm.Ormer {
func ClearTable(table string) error {
o := GetOrmer()
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()
return err
}

View File

@ -19,6 +19,9 @@ import (
"time"
)
// ProjectTable is the table name for project
const ProjectTable = "project"
// Project holds the details of a project.
type Project struct {
ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"`
@ -174,3 +177,8 @@ type ProjectQueryResult struct {
Total int64
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"
)
// UserTable is the name of table in DB that holds the user object
const UserTable = "user"
// User holds the details of a user.
type User struct {
UserID int `orm:"pk;auto;column(user_id)" json:"user_id"`
@ -45,3 +48,8 @@ type UserQuery struct {
Email string
Pagination *Pagination
}
// TableName ...
func (u *User) TableName() string {
return UserTable
}

View File

@ -19,12 +19,25 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/vmware/harbor/src/common/utils/log"
"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.
@ -33,6 +46,8 @@ type Client interface {
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.
GetUserInfo(token string) (*UserInfo, error)
//SearchUser searches a user based on user name.
SearchUser(name string) ([]*SearchUserEntry, error)
}
// ClientConfig values to initialize UAA Client
@ -56,21 +71,43 @@ type UserInfo struct {
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
type defaultClient struct {
httpClient *http.Client
oauth2Cfg *oauth2.Config
twoLegCfg *clientcredentials.Config
endpoint string
//TODO: add public key, etc...
}
func (dc *defaultClient) PasswordAuth(username, password string) (*oauth2.Token, error) {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient)
return dc.oauth2Cfg.PasswordCredentialsToken(ctx, username, password)
return dc.oauth2Cfg.PasswordCredentialsToken(dc.prepareCtx(), username, password)
}
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)
if err != nil {
return nil, err
@ -92,6 +129,45 @@ func (dc *defaultClient) GetUserInfo(token string) (*UserInfo, error) {
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.
func NewDefaultClient(cfg *ClientConfig) (Client, error) {
url := cfg.Endpoint
@ -125,14 +201,21 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: url + "/uaa/oauth/token",
AuthURL: url + "/uaa/oauth/authorize",
TokenURL: url + TokenURLSuffix,
AuthURL: url + AuthURLSuffix,
},
}
cc := &clientcredentials.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
TokenURL: url + TokenURLSuffix,
}
return &defaultClient{
httpClient: hc,
oauth2Cfg: oc,
twoLegCfg: cc,
endpoint: url,
}, nil
}

View File

@ -30,15 +30,18 @@ func TestMain(m *testing.M) {
}
}
func TestPasswordAuth(t *testing.T) {
cfg := &ClientConfig{
func getCfg() *ClientConfig {
return &ClientConfig{
ClientID: "uaa",
ClientSecret: "secret",
Endpoint: mockUAAServer.URL,
SkipTLSVerify: true,
}
}
func TestPasswordAuth(t *testing.T) {
assert := assert.New(t)
client, err := NewDefaultClient(cfg)
client, err := NewDefaultClient(getCfg())
assert.Nil(err)
_, err = client.PasswordAuth("user1", "pass1")
assert.Nil(err)
@ -47,14 +50,8 @@ func TestPasswordAuth(t *testing.T) {
}
func TestUserInfo(t *testing.T) {
cfg := &ClientConfig{
ClientID: "uaa",
ClientSecret: "secret",
Endpoint: mockUAAServer.URL,
SkipTLSVerify: true,
}
assert := assert.New(t)
client, err := NewDefaultClient(cfg)
client, err := NewDefaultClient(getCfg())
assert.Nil(err)
token, err := ioutil.ReadFile(path.Join(currPath(), "test", "./good-access-token.txt"))
if err != nil {
@ -68,6 +65,21 @@ func TestUserInfo(t *testing.T) {
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 {
_, f, _, ok := runtime.Caller(0)
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) {
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,23 +53,36 @@ func (t *tokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
http.Error(rw, "invalid client id/secret in header", http.StatusUnauthorized)
return
}
if gt := req.FormValue("grant_type"); gt != "password" {
gt := req.FormValue("grant_type")
if gt == "password" {
reqUsername := req.FormValue("username")
reqPasswd := req.FormValue("password")
if reqUsername == t.username && reqPasswd == t.password {
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
}
reqUsername := req.FormValue("username")
reqPasswd := req.FormValue("password")
if reqUsername == t.username && reqPasswd == t.password {
token, err := ioutil.ReadFile(path.Join(currPath(), "./uaa-token.json"))
if err != nil {
panic(err)
}
_, err2 := rw.Write(token)
if err2 != nil {
panic(err2)
}
} else {
http.Error(rw, fmt.Sprintf("invalid username/password %s/%s", reqUsername, reqPasswd), http.StatusUnauthorized)
}
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 {
panic(err)
}
rw.Header().Add("Content-Type", "application/json")
_, err2 := rw.Write(data)
if err2 != nil {
panic(err2)
}
}
@ -78,27 +91,52 @@ type userInfoHandler struct {
}
func (u *userInfoHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
v := req.Header.Get("Authorization")
v := req.Header.Get("authorization")
prefix := v[0:7]
reqToken := v[7:]
if strings.ToLower(prefix) != "bearer " || reqToken != u.token {
http.Error(rw, "invalid token", http.StatusUnauthorized)
return
}
userInfo, err := ioutil.ReadFile(path.Join(currPath(), "./user-info.json"))
if err != nil {
panic(err)
serveJSONFile(rw, "./user-info.json")
}
type searchUserHandler struct {
token string
}
func (su *searchUserHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
v := req.Header.Get("authorization")
if v == "" {
v = req.Header.Get("Authorization")
}
_, err2 := rw.Write(userInfo)
if err2 != nil {
panic(err2)
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 ...
func NewMockServer(cfg *MockServerConfig) *httptest.Server {
mux := http.NewServeMux()
mux.Handle("/uaa/oauth/token", &tokenHandler{
mux.Handle("/oauth/token", &tokenHandler{
cfg.ClientID,
cfg.ClientSecret,
cfg.Username,
@ -108,6 +146,7 @@ func NewMockServer(cfg *MockServerConfig) *httptest.Server {
if err != nil {
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)
}

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.
func Register(name string, h AuthenticateHelper) {
if _, dup := registry[name]; dup {
log.Infof("authenticator: %s has been registered", name)
log.Infof("authenticator: %s has been registered,skip", name)
return
}
registry[name] = h
log.Debugf("Registered authencation helper for auth mode: %s", name)
}
// Login authenticates user credentials based on setting.

View File

@ -15,24 +15,20 @@
package uaa
import (
"fmt"
"strings"
"sync"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/uaa"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
)
var lock = &sync.Mutex{}
var client uaa.Client
//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
}
//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
@ -43,51 +39,94 @@ func GetClient() (uaa.Client, error) {
Endpoint: UAASettings.Endpoint,
SkipTLSVerify: !UAASettings.VerifyCert,
}
client, err = uaa.NewDefaultClient(cfg)
return client, err
return uaa.NewDefaultClient(cfg)
}
func doAuth(username, password string, client uaa.Client) (*models.User, error) {
t, err := client.PasswordAuth(username, password)
// Auth is the implementation of AuthenticateHelper to access uaa for authentication.
type Auth struct {
sync.Mutex
client uaa.Client
}
//Authenticate ...
func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
if err := u.ensureClient(); err != nil {
return nil, err
}
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.
u := &models.User{
Username: username,
Password: "1234567ab",
Email: username + "@placeholder.com",
Realname: username,
}
err = dao.OnBoardUser(u)
if err == nil {
return u, nil
user := &models.User{
Username: m.Principal,
}
err = u.OnBoardUser(user)
return user, err
}
return nil, err
}
// Auth is the implementation of AuthenticateHelper to access uaa for authentication.
type Auth struct{}
// 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.
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")
}
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)
}
//Authenticate ...
func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
client, err := GetClient()
// SearchUser search user on uaa server, transform it to Harbor's user model
func (u *Auth) SearchUser(username string) (*models.User, error) {
if err := u.ensureClient(); err != nil {
return nil, err
}
l, err := u.client.SearchUser(username)
if err != nil {
return nil, err
}
return doAuth(m.Principal, m.Password, client)
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
}
// 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.
// func (u *Auth) OnBoardUser(user *models.User) error {
// panic("not implemented")
// }
// // SearchUser - search user on uaa server
// func (u *Auth) SearchUser(username string) (*models.User, error) {
// panic("not implemented")
// }
// func init() {
// auth.Register(auth.UAAAuth, &Auth{})
// }
func (u *Auth) ensureClient() error {
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 (
"github.com/stretchr/testify/assert"
"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"
"github.com/vmware/harbor/src/common/utils/uaa"
"github.com/vmware/harbor/src/ui/config"
"os"
"strconv"
"testing"
)
func TestGetClient(t *testing.T) {
assert := assert.New(t)
func TestMain(m *testing.M) {
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)
if err != nil {
t.Fatalf("failed to create a mock admin server: %v", err)
panic(err)
}
defer server.Close()
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()
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.NotNil(c)
}
func TestDoAuth(t *testing.T) {
func TestAuthenticate(t *testing.T) {
assert := assert.New(t)
client := &uaa.FakeClient{
Username: "user1",
Password: "password1",
}
dao.PrepareTestForMySQL()
u1, err1 := doAuth("user1", "password1", client)
auth := Auth{client: client}
m1 := models.AuthModel{
Principal: "user1",
Password: "password1",
}
u1, err1 := auth.Authenticate(m1)
assert.Nil(err1)
assert.True(u1.UserID > 0)
u2, err2 := doAuth("wrong", "wrong", client)
assert.NotNil(u1)
m2 := models.AuthModel{
Principal: "wrong",
Password: "wrong",
}
u2, err2 := auth.Authenticate(m2)
assert.NotNil(err2)
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)
}