From f6476b62f2c8f1e64e6cff00f63f30500628fa73 Mon Sep 17 00:00:00 2001 From: Ken Perkins Date: Mon, 6 Apr 2020 06:40:17 -0700 Subject: [PATCH] Added Email of Keystone to Identity (#1681) * Added Email of Keystone to Identity After the successful login to keystone, the Email of the logged in user is fetch from keystone and provided to `identity.Email`. This is useful for upstream software that uses the Email as the primary identification. * Removed unnecessary code from getUsers * Changed creation of userResponse in keystone * Fixing linter error Co-authored-by: Christoph Glaubitz --- connector/keystone/keystone.go | 45 +++++++++-- connector/keystone/keystone_test.go | 113 +++++++++++++++++++++------- 2 files changed, 125 insertions(+), 33 deletions(-) diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go index 6ce22d1a..92682f78 100644 --- a/connector/keystone/keystone.go +++ b/connector/keystone/keystone.go @@ -95,6 +95,14 @@ type groupsResponse struct { Groups []group `json:"groups"` } +type userResponse struct { + User struct { + Name string `json:"name"` + Email string `json:"email"` + ID string `json:"id"` + } `json:"user"` +} + var ( _ connector.PasswordConnector = &conn{} _ connector.RefreshConnector = &conn{} @@ -143,6 +151,16 @@ func (p *conn) Login(ctx context.Context, scopes connector.Scopes, username, pas } identity.Username = username identity.UserID = tokenResp.Token.User.ID + + user, err := p.getUser(ctx, tokenResp.Token.User.ID, token) + if err != nil { + return identity, false, err + } + if user.User.Email != "" { + identity.Email = user.User.Email + identity.EmailVerified = true + } + return identity, true, nil } @@ -216,26 +234,43 @@ func (p *conn) getAdminToken(ctx context.Context) (string, error) { } func (p *conn) checkIfUserExists(ctx context.Context, userID string, token string) (bool, error) { + user, err := p.getUser(ctx, userID, token) + return user != nil, err +} + +func (p *conn) getUser(ctx context.Context, userID string, token string) (*userResponse, error) { // https://developer.openstack.org/api-ref/identity/v3/#show-user-details userURL := p.Host + "/v3/users/" + userID client := &http.Client{} req, err := http.NewRequest("GET", userURL, nil) if err != nil { - return false, err + return nil, err } req.Header.Set("X-Auth-Token", token) req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { - return false, err + return nil, err } defer resp.Body.Close() - if resp.StatusCode == 200 { - return true, nil + if resp.StatusCode != 200 { + return nil, err } - return false, err + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + user := userResponse{} + err = json.Unmarshal(data, &user) + if err != nil { + return nil, err + } + + return &user, nil } func (p *conn) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) { diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go index 784cdc4f..d66da8be 100644 --- a/connector/keystone/keystone_test.go +++ b/connector/keystone/keystone_test.go @@ -35,12 +35,6 @@ var ( groupsURL = "" ) -type userResponse struct { - User struct { - ID string `json:"id"` - } `json:"user"` -} - type groupResponse struct { Group struct { ID string `json:"id"` @@ -144,7 +138,7 @@ func createUser(t *testing.T, token, userName, userEmail, userPass string) strin } // delete group or user -func delete(t *testing.T, token, id, uri string) { +func deleteResource(t *testing.T, token, id, uri string) { t.Helper() client := &http.Client{} @@ -246,20 +240,86 @@ func TestIncorrectCredentialsLogin(t *testing.T) { func TestValidUserLogin(t *testing.T) { setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) - userID := createUser(t, token, testUser, testEmail, testPass) - c := conn{Host: keystoneURL, Domain: testDomain, - AdminUsername: adminUser, AdminPassword: adminPass} - s := connector.Scopes{OfflineAccess: true, Groups: true} - identity, validPW, err := c.Login(context.Background(), s, testUser, testPass) - if err != nil { - t.Fatal(err.Error()) - } - t.Log(identity) - if !validPW { - t.Fatal("Valid password was not accepted") + type tUser struct { + username string + domain string + email string + password string + } + + type expect struct { + username string + email string + verifiedEmail bool + } + + var tests = []struct { + name string + input tUser + expected expect + }{ + { + name: "test with email address", + input: tUser{ + username: testUser, + domain: testDomain, + email: testEmail, + password: testPass, + }, + expected: expect{ + username: testUser, + email: testEmail, + verifiedEmail: true, + }, + }, + { + name: "test without email address", + input: tUser{ + username: testUser, + domain: testDomain, + email: "", + password: testPass, + }, + expected: expect{ + username: testUser, + email: "", + verifiedEmail: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID := createUser(t, token, tt.input.username, tt.input.email, tt.input.password) + defer deleteResource(t, token, userID, usersURL) + + c := conn{Host: keystoneURL, Domain: tt.input.domain, + AdminUsername: adminUser, AdminPassword: adminPass} + s := connector.Scopes{OfflineAccess: true, Groups: true} + identity, validPW, err := c.Login(context.Background(), s, tt.input.username, tt.input.password) + if err != nil { + t.Fatal(err.Error()) + } + t.Log(identity) + if identity.Username != tt.expected.username { + t.Fatalf("Invalid user. Got: %v. Wanted: %v", identity.Username, tt.expected.username) + } + if identity.UserID == "" { + t.Fatalf("Didn't get any UserID back") + } + if identity.Email != tt.expected.email { + t.Fatalf("Invalid email. Got: %v. Wanted: %v", identity.Email, tt.expected.email) + } + if identity.EmailVerified != tt.expected.verifiedEmail { + t.Fatalf("Invalid verifiedEmail. Got: %v. Wanted: %v", identity.EmailVerified, tt.expected.verifiedEmail) + } + + if !validPW { + t.Fatal("Valid password was not accepted") + } + }) } - delete(t, token, userID, usersURL) } func TestUseRefreshToken(t *testing.T) { @@ -267,6 +327,7 @@ func TestUseRefreshToken(t *testing.T) { token, adminID := getAdminToken(t, adminUser, adminPass) groupID := createGroup(t, token, "Test group description", testGroup) addUserToGroup(t, token, groupID, adminID) + defer deleteResource(t, token, groupID, groupsURL) c := conn{Host: keystoneURL, Domain: testDomain, AdminUsername: adminUser, AdminPassword: adminPass} @@ -282,8 +343,6 @@ func TestUseRefreshToken(t *testing.T) { t.Fatal(err.Error()) } - delete(t, token, groupID, groupsURL) - expectEquals(t, 1, len(identityRefresh.Groups)) expectEquals(t, testGroup, identityRefresh.Groups[0]) } @@ -307,7 +366,7 @@ func TestUseRefreshTokenUserDeleted(t *testing.T) { t.Fatal(err.Error()) } - delete(t, token, userID, usersURL) + deleteResource(t, token, userID, usersURL) _, err = c.Refresh(context.Background(), s, identityLogin) if !strings.Contains(err.Error(), "does not exist") { @@ -319,6 +378,7 @@ func TestUseRefreshTokenGroupsChanged(t *testing.T) { setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) userID := createUser(t, token, testUser, testEmail, testPass) + defer deleteResource(t, token, userID, usersURL) c := conn{Host: keystoneURL, Domain: testDomain, AdminUsername: adminUser, AdminPassword: adminPass} @@ -338,15 +398,13 @@ func TestUseRefreshTokenGroupsChanged(t *testing.T) { groupID := createGroup(t, token, "Test group", testGroup) addUserToGroup(t, token, groupID, userID) + defer deleteResource(t, token, groupID, groupsURL) identityRefresh, err = c.Refresh(context.Background(), s, identityLogin) if err != nil { t.Fatal(err.Error()) } - delete(t, token, groupID, groupsURL) - delete(t, token, userID, usersURL) - expectEquals(t, 1, len(identityRefresh.Groups)) } @@ -354,6 +412,7 @@ func TestNoGroupsInScope(t *testing.T) { setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) userID := createUser(t, token, testUser, testEmail, testPass) + defer deleteResource(t, token, userID, usersURL) c := conn{Host: keystoneURL, Domain: testDomain, AdminUsername: adminUser, AdminPassword: adminPass} @@ -361,6 +420,7 @@ func TestNoGroupsInScope(t *testing.T) { groupID := createGroup(t, token, "Test group", testGroup) addUserToGroup(t, token, groupID, userID) + defer deleteResource(t, token, groupID, groupsURL) identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass) if err != nil { @@ -373,9 +433,6 @@ func TestNoGroupsInScope(t *testing.T) { t.Fatal(err.Error()) } expectEquals(t, 0, len(identityRefresh.Groups)) - - delete(t, token, groupID, groupsURL) - delete(t, token, userID, usersURL) } func setupVariables(t *testing.T) {