Repository: Nerzal/gocloak
Pull request #54 (2019-05-21 04:00:29)

List of user sessions
Add functions to work with user sessions:
  * GetClientUserSessions
  * GetClientOfflineSessions
  * GetUserSessions
  * GetUserOfflineSessionsForClient


SVilgelm: Sure, also I'm going to add unit tests
codecov[bot]: # [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/54?src=pr&el=h1) Report
> Merging [#54](https://codecov.io/gh/Nerzal/gocloak/pull/54?src=pr&el=desc) into [master](https://codecov.io/gh/Nerzal/gocloak/commit/60ca0132653823ecb25d7941e6c29810628100c0?src=pr&el=desc) will **increase** coverage by `0.15%`.
> The diff coverage is `75%`.

[![Impacted file tree graph](https://codecov.io/gh/Nerzal/gocloak/pull/54/graphs/tree.svg?width=650&token=gB52LSVNIV&height=150&src=pr)](https://codecov.io/gh/Nerzal/gocloak/pull/54?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master     #54      +/-   ##
=========================================
+ Coverage   72.24%   72.4%   +0.15%     
=========================================
  Files           3       3              
  Lines         526     558      +32     
=========================================
+ Hits          380     404      +24     
- Misses        110     114       +4     
- Partials       36      40       +4
```


| [Impacted Files](https://codecov.io/gh/Nerzal/gocloak/pull/54?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [models.go](https://codecov.io/gh/Nerzal/gocloak/pull/54/diff?src=pr&el=tree#diff-bW9kZWxzLmdv) | `72.72% <ø> (ø)` | :arrow_up: |
| [client.go](https://codecov.io/gh/Nerzal/gocloak/pull/54/diff?src=pr&el=tree#diff-Y2xpZW50Lmdv) | `72.65% <75%> (+0.14%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/54?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/54?src=pr&el=footer). Last update [60ca013...2aee40f](https://codecov.io/gh/Nerzal/gocloak/pull/54?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 9dc24b9..a704c7b 100644
--- a/README.md
+++ b/README.md
@@ -149,6 +149,11 @@ type GoCloak interface {
 	GetRealm(token string, realm string) (*RealmRepresentation, error)
 	CreateRealm(token string, realm RealmRepresentation) error
 	DeleteRealm(token string, realm string) error
+
+	GetClientUserSessions(token, realm, clientID string) (*[]UserSessionRepresentation, error)
+	GetClientOfflineSessions(token, realm, clientID string) (*[]UserSessionRepresentation, error)
+	GetUserSessions(token, realm, userID string) (*[]UserSessionRepresentation, error)
+	GetUserOfflineSessionsForClient(token, realm, userID, clientID string) (*[]UserSessionRepresentation, error)
 }
 ```
 
diff --git a/client.go b/client.go
index 5d1cfa9..bb339bb 100644
--- a/client.go
+++ b/client.go
@@ -467,6 +467,32 @@ func (client *gocloak) GetClientSecret(token string, realm string, clientID stri
 	return &result, nil
 }
 
+// GetClientOfflineSessions returns offline sessions associated with the client
+func (client *gocloak) GetClientOfflineSessions(token, realm, clientID string) (*[]UserSessionRepresentation, error) {
+	var res []UserSessionRepresentation
+	resp, err := client.getRequestWithBearerAuth(token).
+		SetResult(&res).
+		Get(client.getAdminRealmURL(realm, "clients", clientID, "offline-sessions"))
+
+	if err := checkForError(resp, err); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
+
+// GetClientUserSessions returns user sessions associated with the client
+func (client *gocloak) GetClientUserSessions(token, realm, clientID string) (*[]UserSessionRepresentation, error) {
+	var res []UserSessionRepresentation
+	resp, err := client.getRequestWithBearerAuth(token).
+		SetResult(&res).
+		Get(client.getAdminRealmURL(realm, "clients", clientID, "user-sessions"))
+
+	if err := checkForError(resp, err); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
+
 // GetKeyStoreConfig get keystoreconfig of the realm
 func (client *gocloak) GetKeyStoreConfig(token string, realm string) (*KeyStoreConfig, error) {
 	var result KeyStoreConfig
@@ -907,3 +933,29 @@ func (client *gocloak) DeleteUserFromGroup(token string, realm string, userID st
 
 	return checkForError(resp, err)
 }
+
+// GetUserSessions returns user sessions associated with the user
+func (client *gocloak) GetUserSessions(token, realm, userID string) (*[]UserSessionRepresentation, error) {
+	var res []UserSessionRepresentation
+	resp, err := client.getRequestWithBearerAuth(token).
+		SetResult(&res).
+		Get(client.getAdminRealmURL(realm, "users", userID, "sessions"))
+
+	if err := checkForError(resp, err); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
+
+// GetUserOfflineSessionsForClient returns offline sessions associated with the user and client
+func (client *gocloak) GetUserOfflineSessionsForClient(token, realm, userID, clientID string) (*[]UserSessionRepresentation, error) {
+	var res []UserSessionRepresentation
+	resp, err := client.getRequestWithBearerAuth(token).
+		SetResult(&res).
+		Get(client.getAdminRealmURL(realm, "users", userID, "offline-sessions", clientID))
+
+	if err := checkForError(resp, err); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
diff --git a/gocloak.go b/gocloak.go
index db99dcc..20bb2a0 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -93,6 +93,10 @@ type GoCloak interface {
 	GetClientRole(token string, realm string, clientID string, roleName string) (*Role, error)
 	// GetClients gets the clients in the realm
 	GetClients(accessToken string, realm string, params GetClientsParams) (*[]Client, error)
+	// GetClientOfflineSessions returns offline sessions associated with the client
+	GetClientOfflineSessions(token, realm, clientID string) (*[]UserSessionRepresentation, error)
+	// GetClientUserSessions returns user sessions associated with the client
+	GetClientUserSessions(token, realm, clientID string) (*[]UserSessionRepresentation, error)
 
 	// UserAttributeContains checks if the given attribute has the given value
 	UserAttributeContains(attributes map[string][]string, attribute string, value string) bool
@@ -154,4 +158,8 @@ type GoCloak interface {
 	AddUserToGroup(token string, realm string, userID string, groupID string) error
 	// DeleteUserFromGroup deletes given user from given group
 	DeleteUserFromGroup(token string, realm string, userID string, groupID string) error
+	// GetUserSessions returns user sessions associated with the user
+	GetUserSessions(token, realm, userID string) (*[]UserSessionRepresentation, error)
+	// GetUserOfflineSessionsForClient returns offline sessions associated with the user and client
+	GetUserOfflineSessionsForClient(token, realm, userID, clientID string) (*[]UserSessionRepresentation, error)
 }
diff --git a/models.go b/models.go
index 4cbdd55..a303570 100644
--- a/models.go
+++ b/models.go
@@ -548,3 +548,14 @@ func (t *TokenOptions) FormData() map[string]string {
 	_ = json.Unmarshal(m, &res)
 	return res
 }
+
+// UserSessionRepresentation represents a list of user's sessions
+type UserSessionRepresentation struct {
+	Clients    map[string]string `json:"clients,omitempty"`
+	ID         string            `json:"id,omitempty"`
+	IPAddress  string            `json:"ipAddress,omitempty"`
+	LastAccess int64             `json:"lastAccess,omitempty"`
+	Start      int64             `json:"start,omitempty"`
+	UserID     string            `json:"userId,omitempty"`
+	Username   string            `json:"username,omitempty"`
+}


Test patch:
diff --git a/client_test.go b/client_test.go
index bcba78a..96c6b82 100644
--- a/client_test.go
+++ b/client_test.go
@@ -46,6 +46,11 @@ var (
 	config     *Config
 	configOnce sync.Once
 	setupOnce  sync.Once
+	testUserID string
+)
+
+const (
+	gocloakClientID = "60be66a5-e007-464c-9b74-0e3c2e69e478"
 )
 
 func FailIfErr(t *testing.T, err error, msg string) {
@@ -218,7 +223,6 @@ func SetUpTestUser(t *testing.T, client GoCloak) {
 			token.AccessToken,
 			cfg.GoCloak.Realm,
 			user)
-		var userID string
 		if err != nil && err.Error() == "Conflict: Object already exists" {
 			err = nil
 			users, err := client.GetUsers(
@@ -230,18 +234,18 @@ func SetUpTestUser(t *testing.T, client GoCloak) {
 			FailIfErr(t, err, "GetUsers failed")
 			for _, user := range *users {
 				if user.Username == cfg.GoCloak.UserName {
-					userID = user.ID
+					testUserID = user.ID
 					break
 				}
 			}
 		} else {
 			FailIfErr(t, err, "CreateUser failed")
-			userID = *createdUserID
+			testUserID = *createdUserID
 		}
 
 		err = client.SetPassword(
 			token.AccessToken,
-			userID,
+			testUserID,
 			cfg.GoCloak.Realm,
 			cfg.GoCloak.Password,
 			false)
@@ -503,7 +507,6 @@ func TestGocloak_Login(t *testing.T) {
 
 func TestGocloak_GetToken(t *testing.T) {
 	t.Parallel()
-	t.Failed()
 	cfg := GetConfig(t)
 	client := NewClientWithDebug(t)
 	SetUpTestUser(t, client)
@@ -1360,3 +1363,112 @@ func TestGocloak_GetUsersByRoleName(t *testing.T) {
 		userID,
 		(*users)[0].ID)
 }
+
+func TestGocloak_GetUserSessions(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	SetUpTestUser(t, client)
+	_, err := client.GetToken(
+		cfg.GoCloak.Realm,
+		TokenOptions{
+			ClientID:      cfg.GoCloak.ClientID,
+			ClientSecret:  cfg.GoCloak.ClientSecret,
+			Username:      cfg.GoCloak.UserName,
+			Password:      cfg.GoCloak.Password,
+			GrantType:     "password",
+		},
+	)
+	FailIfErr(t, err, "Login failed")
+	token := GetAdminToken(t, client)
+	sessions, err := client.GetUserSessions(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		testUserID,
+	)
+	FailIfErr(t, err, "GetUserSessions failed")
+	FailIf(t, len(*sessions) == 0, "GetUserSessions returned an empty list")
+}
+
+func TestGocloak_GetUserOfflineSessionsForClient(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	SetUpTestUser(t, client)
+	_, err := client.GetToken(
+		cfg.GoCloak.Realm,
+		TokenOptions{
+			ClientID:      cfg.GoCloak.ClientID,
+			ClientSecret:  cfg.GoCloak.ClientSecret,
+			Username:      cfg.GoCloak.UserName,
+			Password:      cfg.GoCloak.Password,
+			GrantType:     "password",
+			ResponseTypes: []string{"token", "id_token"},
+			Scopes:        []string{"openid", "offline_access"},
+		},
+	)
+	FailIfErr(t, err, "Login failed")
+	token := GetAdminToken(t, client)
+	sessions, err := client.GetUserOfflineSessionsForClient(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		testUserID,
+		gocloakClientID,
+	)
+	FailIfErr(t, err, "GetUserOfflineSessionsForClient failed")
+	FailIf(t, len(*sessions) == 0, "GetUserOfflineSessionsForClient returned an empty list")
+}
+
+func TestGocloak_GetClientUserSessions(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	SetUpTestUser(t, client)
+	_, err := client.GetToken(
+		cfg.GoCloak.Realm,
+		TokenOptions{
+			ClientID:      cfg.GoCloak.ClientID,
+			ClientSecret:  cfg.GoCloak.ClientSecret,
+			Username:      cfg.GoCloak.UserName,
+			Password:      cfg.GoCloak.Password,
+			GrantType:     "password",
+		},
+	)
+	FailIfErr(t, err, "Login failed")
+	token := GetAdminToken(t, client)
+	sessions, err := client.GetClientUserSessions(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+	)
+	FailIfErr(t, err, "GetClientUserSessions failed")
+	FailIf(t, len(*sessions) == 0, "GetClientUserSessions returned an empty list")
+}
+
+func TestGocloak_GetClientOfflineSessions(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	SetUpTestUser(t, client)
+	_, err := client.GetToken(
+		cfg.GoCloak.Realm,
+		TokenOptions{
+			ClientID:      cfg.GoCloak.ClientID,
+			ClientSecret:  cfg.GoCloak.ClientSecret,
+			Username:      cfg.GoCloak.UserName,
+			Password:      cfg.GoCloak.Password,
+			GrantType:     "password",
+			ResponseTypes: []string{"token", "id_token"},
+			Scopes:        []string{"openid", "offline_access"},
+		},
+	)
+	FailIfErr(t, err, "Login failed")
+	token := GetAdminToken(t, client)
+	sessions, err := client.GetClientOfflineSessions(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+	)
+	FailIfErr(t, err, "GetClientOfflineSessions failed")
+	FailIf(t, len(*sessions) == 0, "GetClientOfflineSessions returned an empty list")
+}