From d13321f2b5d6d6f2e4ed822efc78cc1aff089dde Mon Sep 17 00:00:00 2001 From: Daniel Jiang Date: Mon, 27 Nov 2017 18:13:36 +0800 Subject: [PATCH] Support getting user info via token in UAA Client (#3686) --- src/common/utils/uaa/client.go | 39 +++++++++++++++++++ src/common/utils/uaa/client_test.go | 25 ++++++++++++ .../utils/uaa/{test => }/fake_client.go | 7 +++- .../utils/uaa/test/good-access-token.txt | 1 + src/common/utils/uaa/test/server.go | 28 +++++++++++++ src/common/utils/uaa/test/user-info.json | 11 ++++++ src/ui/auth/uaa/uaa_test.go | 4 +- 7 files changed, 112 insertions(+), 3 deletions(-) rename src/common/utils/uaa/{test => }/fake_client.go (89%) create mode 100644 src/common/utils/uaa/test/good-access-token.txt create mode 100644 src/common/utils/uaa/test/user-info.json diff --git a/src/common/utils/uaa/client.go b/src/common/utils/uaa/client.go index d0f259bfa..0f6511f5d 100644 --- a/src/common/utils/uaa/client.go +++ b/src/common/utils/uaa/client.go @@ -18,6 +18,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "io/ioutil" "net/http" "strings" @@ -30,6 +31,8 @@ import ( type Client interface { //PasswordAuth accepts username and password, return a token if it's valid. 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) } // ClientConfig values to initialize UAA Client @@ -42,10 +45,22 @@ type ClientConfig struct { CARootPath string } +// UserInfo represent the JSON object of a userinfo response from UAA. +// As the response varies, this struct will contain only a subset of attributes +// that may be used in Harbor +type UserInfo struct { + UserID string `json:"user_id"` + Sub string `json:"sub"` + UserName string `json:"user_name"` + Name string `json:"name"` + Email string `json:"email"` +} + // DefaultClient leverages oauth2 pacakge for oauth features type defaultClient struct { httpClient *http.Client oauth2Cfg *oauth2.Config + endpoint string //TODO: add public key, etc... } @@ -54,6 +69,29 @@ func (dc *defaultClient) PasswordAuth(username, password string) (*oauth2.Token, return dc.oauth2Cfg.PasswordCredentialsToken(ctx, username, password) } +func (dc *defaultClient) GetUserInfo(token string) (*UserInfo, error) { + userInfoURL := dc.endpoint + "/uaa/userinfo" + req, err := http.NewRequest(http.MethodGet, userInfoURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "bearer "+token) + resp, err := dc.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + info := &UserInfo{} + if err := json.Unmarshal(data, info); err != nil { + return nil, err + } + return info, nil +} + // NewDefaultClient creates an instance of defaultClient. func NewDefaultClient(cfg *ClientConfig) (Client, error) { url := cfg.Endpoint @@ -95,5 +133,6 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) { return &defaultClient{ httpClient: hc, oauth2Cfg: oc, + endpoint: url, }, nil } diff --git a/src/common/utils/uaa/client_test.go b/src/common/utils/uaa/client_test.go index 1a8081bb5..ead0c0273 100644 --- a/src/common/utils/uaa/client_test.go +++ b/src/common/utils/uaa/client_test.go @@ -1,12 +1,15 @@ package uaa import ( + "fmt" "github.com/stretchr/testify/assert" "github.com/vmware/harbor/src/common/utils/uaa/test" + "io/ioutil" "net/http/httptest" "os" "path" "runtime" + "strings" "testing" ) @@ -43,6 +46,28 @@ func TestPasswordAuth(t *testing.T) { assert.NotNil(err) } +func TestUserInfo(t *testing.T) { + cfg := &ClientConfig{ + ClientID: "uaa", + ClientSecret: "secret", + Endpoint: mockUAAServer.URL, + SkipTLSVerify: true, + } + assert := assert.New(t) + client, err := NewDefaultClient(cfg) + assert.Nil(err) + token, err := ioutil.ReadFile(path.Join(currPath(), "test", "./good-access-token.txt")) + if err != nil { + panic(err) + } + userInfo, err := client.GetUserInfo(strings.TrimSpace(string(token))) + assert.Nil(err, fmt.Sprintf("%v", err)) + + assert.Equal("user01", userInfo.UserName) + _, err2 := client.GetUserInfo("bad") + assert.NotNil(err2) +} + func currPath() string { _, f, _, ok := runtime.Caller(0) if !ok { diff --git a/src/common/utils/uaa/test/fake_client.go b/src/common/utils/uaa/fake_client.go similarity index 89% rename from src/common/utils/uaa/test/fake_client.go rename to src/common/utils/uaa/fake_client.go index 439a3a177..6e02bb1b6 100644 --- a/src/common/utils/uaa/test/fake_client.go +++ b/src/common/utils/uaa/fake_client.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package test +package uaa import ( "fmt" @@ -32,3 +32,8 @@ func (fc *FakeClient) PasswordAuth(username, password string) (*oauth2.Token, er } return nil, fmt.Errorf("Invalide username and password") } + +// GetUserInfo ... +func (fc *FakeClient) GetUserInfo(token string) (*UserInfo, error) { + return nil, nil +} diff --git a/src/common/utils/uaa/test/good-access-token.txt b/src/common/utils/uaa/test/good-access-token.txt new file mode 100644 index 000000000..2c63bb1d2 --- /dev/null +++ b/src/common/utils/uaa/test/good-access-token.txt @@ -0,0 +1 @@ +eyJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNmRjYjg1YzMzZjU0OGM5ODk2YjI4MDEwN2IyOWM0NiIsInN1YiI6IjlhMTM0ODhmLWYzY2YtNDdhNi05OGYwLTRmZWQyMWY0MzUyMCIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiOWExMzQ4OGYtZjNjZi00N2E2LTk4ZjAtNGZlZDIxZjQzNTIwIiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6InVzZXIwMSIsImVtYWlsIjoidXNlcjAxQHVzZXIuZnJvbS5sZGFwLmNmIiwiYXV0aF90aW1lIjoxNTExNDA1NDEwLCJyZXZfc2lnIjoiOGEwYmY5OWQiLCJpYXQiOjE1MTE0MDU0MTAsImV4cCI6MTUxMTQ0ODYxMCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0My91YWEvb2F1dGgvdG9rZW4iLCJ6aWQiOiJ1YWEiLCJhdWQiOlsia3ViZXJuZXRlcyIsIm9wZW5pZCJdfQ.I7VBx_cQoYkotRJ8KdmESAf_xjzp-R44BRz9ngHPUnoqr4rSMin-Ful8wNzEnaYaG56_mrIPuLOb6vXGWW1svRU892GOK9WQRSiFp7O81V7f1bH6JXnIGvyBNl3JOkDB9d5wXn137h9vNKq3Z9TF3jD7oXR_OENS8paclW5EAjmjGvEVIhObMmHCLhsJshTWIoP8AwoP1m9iqak_-t0c99HWaf1AgVUtT2i9Jb63ndJGA6BkOSRH_YxXmM_qtXmk_0kRA5oLDR2UGA4TVXCYp1_8iwQYjvGBVxO24I5jJh_zDYs5YLTFeNzMTPEhAl_Te6NiE91gRXq6KiVk9tTfuA diff --git a/src/common/utils/uaa/test/server.go b/src/common/utils/uaa/test/server.go index c6b624f63..5f6e1737c 100644 --- a/src/common/utils/uaa/test/server.go +++ b/src/common/utils/uaa/test/server.go @@ -21,6 +21,7 @@ import ( "net/http/httptest" "path" "runtime" + "strings" ) // MockServerConfig ... @@ -72,6 +73,28 @@ func (t *tokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } +type userInfoHandler struct { + token string +} + +func (u *userInfoHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + 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) + } + _, err2 := rw.Write(userInfo) + if err2 != nil { + panic(err2) + } +} + // NewMockServer ... func NewMockServer(cfg *MockServerConfig) *httptest.Server { mux := http.NewServeMux() @@ -81,5 +104,10 @@ func NewMockServer(cfg *MockServerConfig) *httptest.Server { cfg.Username, cfg.Password, }) + token, err := ioutil.ReadFile(path.Join(currPath(), "./good-access-token.txt")) + if err != nil { + panic(err) + } + mux.Handle("/uaa/userinfo", &userInfoHandler{strings.TrimSpace(string(token))}) return httptest.NewTLSServer(mux) } diff --git a/src/common/utils/uaa/test/user-info.json b/src/common/utils/uaa/test/user-info.json new file mode 100644 index 000000000..b188d97f3 --- /dev/null +++ b/src/common/utils/uaa/test/user-info.json @@ -0,0 +1,11 @@ +{ + "user_id": "9a13488f-f3cf-47a6-98f0-4fed21f43520", + "sub": "9a13488f-f3cf-47a6-98f0-4fed21f43520", + "user_name": "user01", + "given_name": null, + "family_name": null, + "email": "user01@user.from.ldap.cf", + "phone_number": null, + "previous_logon_time": 1511247236160, + "name": "" +} diff --git a/src/ui/auth/uaa/uaa_test.go b/src/ui/auth/uaa/uaa_test.go index 629d8bd77..748c362c1 100644 --- a/src/ui/auth/uaa/uaa_test.go +++ b/src/ui/auth/uaa/uaa_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/vmware/harbor/src/common/dao" utilstest "github.com/vmware/harbor/src/common/utils/test" - uaatest "github.com/vmware/harbor/src/common/utils/uaa/test" + "github.com/vmware/harbor/src/common/utils/uaa" "github.com/vmware/harbor/src/ui/config" "os" @@ -47,7 +47,7 @@ func TestGetClient(t *testing.T) { func TestDoAuth(t *testing.T) { assert := assert.New(t) - client := &uaatest.FakeClient{ + client := &uaa.FakeClient{ Username: "user1", Password: "password1", }