Repository: cloudflare/cloudflare-go
Pull request #1427 (2023-10-26 15:02:40)

access_users: Add ZeroTrust Users & Seats functions.
## Description

<!--
Describe your changes in detail through motivation and context. Why is
this change required? What problem does it solve? If it fixes an open
issue, link to the issue using GitHub's closing issues keywords[1].
-->

The following adds the Cloudflare Zerotrust / Access endpoints for user & seat management. 

The following is a rough draft with comments for feedback. 

## Has your change been tested?

Basic API calls locally against relevant endpoints to ensure a proper response is given. 

## Types of changes

What sort of change does your code introduce/modify?

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] My code follows the code style of this project.
- [X] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/



Patch:
diff --git a/.changelog/1427.txt b/.changelog/1427.txt
new file mode 100644
index 000000000..d948ad913
--- /dev/null
+++ b/.changelog/1427.txt
@@ -0,0 +1,23 @@
+```release-note:enhancement
+access_seats: Add UpdateAccessUserSeat() to list IP Access Rules
+```
+
+```release-note:enhancement
+access_user: Add GetAccessUserActiveSessions() to get all active sessions for a Access/Zero-Trust user.
+```
+
+```release-note:enhancement
+access_user: Add GetAccessUserSingleActiveSession() to get an active session for a Access/Zero-Trust user.
+```
+
+```release-note:enhancement
+access_user: Add GetAccessUserFailedLogins() to get all failed login attempts for a Access/Zero-Trust user.
+```
+
+```release-note:enhancement
+access_user: Add GetAccessUserLastSeenIdentity() to get last seen identity for a Access/Zero-Trust user.
+```
+
+```release-note:enhancement
+access_user: Add ListAccessUsers() to get a list of users for a Access/Zero-Trust account.
+```
diff --git a/access_seats.go b/access_seats.go
new file mode 100644
index 000000000..c1d6bf213
--- /dev/null
+++ b/access_seats.go
@@ -0,0 +1,68 @@
+package cloudflare
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/goccy/go-json"
+)
+
+var errMissingAccessSeatUID = errors.New("missing required access seat UID")
+
+// AccessUpdateAccessUserSeatResult represents a Access User Seat.
+type AccessUpdateAccessUserSeatResult struct {
+	AccessSeat  *bool      `json:"access_seat"`
+	CreatedAt   *time.Time `json:"created_at"`
+	UpdatedAt   *time.Time `json:"updated_at"`
+	GatewaySeat *bool      `json:"gateway_seat"`
+	SeatUID     string     `json:"seat_uid,omitempty"`
+}
+
+// UpdateAccessUserSeatParams represents the update payload for access seats.
+type UpdateAccessUserSeatParams struct {
+	SeatUID     string `json:"seat_uid,omitempty"`
+	AccessSeat  *bool  `json:"access_seat"`
+	GatewaySeat *bool  `json:"gateway_seat"`
+}
+
+// AccessUserSeatResponse represents the response from the access user seat endpoints.
+type UpdateAccessUserSeatResponse struct {
+	Response
+	Result     []AccessUpdateAccessUserSeatResult `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// UpdateAccessUserSeat updates a Access User Seat.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/zero-trust-seats-update-a-user-seat
+func (api *API) UpdateAccessUserSeat(ctx context.Context, rc *ResourceContainer, params UpdateAccessUserSeatParams) ([]AccessUpdateAccessUserSeatResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return []AccessUpdateAccessUserSeatResult{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level)
+	}
+
+	if params.SeatUID == "" {
+		return []AccessUpdateAccessUserSeatResult{}, errMissingAccessSeatUID
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/access/seats",
+		rc.Level,
+		rc.Identifier,
+	)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
+	if err != nil {
+		return []AccessUpdateAccessUserSeatResult{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var updateAccessUserSeatResponse UpdateAccessUserSeatResponse
+	err = json.Unmarshal(res, &updateAccessUserSeatResponse)
+	if err != nil {
+		return []AccessUpdateAccessUserSeatResult{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return updateAccessUserSeatResponse.Result, nil
+}
diff --git a/access_users.go b/access_users.go
new file mode 100644
index 000000000..690bc446e
--- /dev/null
+++ b/access_users.go
@@ -0,0 +1,360 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/goccy/go-json"
+)
+
+type AccessUserActiveSessionsResponse struct {
+	Result     []AccessUserActiveSessionResult `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+type AccessUserActiveSessionResult struct {
+	Expiration int64                           `json:"expiration"`
+	Metadata   AccessUserActiveSessionMetadata `json:"metadata"`
+	Name       string                          `json:"name"`
+}
+
+type AccessUserActiveSessionMetadata struct {
+	Apps    map[string]AccessUserActiveSessionMetadataApp `json:"apps"`
+	Expires int64                                         `json:"expires"`
+	IAT     int64                                         `json:"iat"`
+	Nonce   string                                        `json:"nonce"`
+	TTL     int64                                         `json:"ttl"`
+}
+
+type AccessUserActiveSessionMetadataApp struct {
+	Hostname string `json:"hostname"`
+	Name     string `json:"name"`
+	Type     string `json:"type"`
+	UID      string `json:"uid"`
+}
+
+type AccessUserDevicePosture struct {
+	Check       AccessUserDevicePostureCheck `json:"check"`
+	Data        map[string]interface{}       `json:"data"`
+	Description string                       `json:"description"`
+	Error       string                       `json:"error"`
+	ID          string                       `json:"id"`
+	RuleName    string                       `json:"rule_name"`
+	Success     *bool                        `json:"success"`
+	Timestamp   string                       `json:"timestamp"`
+	Type        string                       `json:"type"`
+}
+
+type AccessUserDeviceSession struct {
+	LastAuthenticated int64 `json:"last_authenticated"`
+}
+
+type AccessUserFailedLoginsResponse struct {
+	Result     []AccessUserFailedLoginResult `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+type AccessUserFailedLoginResult struct {
+	Expiration int64                         `json:"expiration"`
+	Metadata   AccessUserFailedLoginMetadata `json:"metadata"`
+}
+
+type AccessUserFailedLoginMetadata struct {
+	AppName   string `json:"app_name"`
+	Aud       string `json:"aud"`
+	Datetime  string `json:"datetime"`
+	RayID     string `json:"ray_id"`
+	UserEmail string `json:"user_email"`
+	UserUUID  string `json:"user_uuid"`
+}
+
+type AccessUserLastSeenIdentityResponse struct {
+	Result     AccessUserLastSeenIdentityResult `json:"result"`
+	ResultInfo ResultInfo                       `json:"result_info"`
+	Response
+}
+
+type AccessUserLastSeenIdentityResult struct {
+	AccountID          string                             `json:"account_id"`
+	AuthStatus         string                             `json:"auth_status"`
+	CommonName         string                             `json:"common_name"`
+	DeviceID           string                             `json:"device_id"`
+	DevicePosture      map[string]AccessUserDevicePosture `json:"devicePosture"`
+	DeviceSessions     map[string]AccessUserDeviceSession `json:"device_sessions"`
+	Email              string                             `json:"email"`
+	Geo                AccessUserIdentityGeo              `json:"geo"`
+	IAT                int64                              `json:"iat"`
+	IDP                AccessUserIDP                      `json:"idp"`
+	IP                 string                             `json:"ip"`
+	IsGateway          *bool                              `json:"is_gateway"`
+	IsWarp             *bool                              `json:"is_warp"`
+	MtlsAuth           AccessUserMTLSAuth                 `json:"mtls_auth"`
+	ServiceTokenID     string                             `json:"service_token_id"`
+	ServiceTokenStatus *bool                              `json:"service_token_status"`
+	UserUUID           string                             `json:"user_uuid"`
+	Version            int                                `json:"version"`
+}
+
+type AccessUserLastSeenIdentitySessionResponse struct {
+	Result     GetAccessUserLastSeenIdentityResult `json:"result"`
+	ResultInfo ResultInfo                          `json:"result_info"`
+	Response
+}
+
+type GetAccessUserLastSeenIdentityResult struct {
+	AccountID          string                             `json:"account_id"`
+	AuthStatus         string                             `json:"auth_status"`
+	CommonName         string                             `json:"common_name"`
+	DevicePosture      map[string]AccessUserDevicePosture `json:"devicePosture"`
+	DeviceSessions     map[string]AccessUserDeviceSession `json:"device_sessions"`
+	DeviceID           string                             `json:"device_id"`
+	Email              string                             `json:"email"`
+	Geo                AccessUserIdentityGeo              `json:"geo"`
+	IAT                int64                              `json:"iat"`
+	IDP                AccessUserIDP                      `json:"idp"`
+	IP                 string                             `json:"ip"`
+	IsGateway          *bool                              `json:"is_gateway"`
+	IsWarp             *bool                              `json:"is_warp"`
+	MtlsAuth           AccessUserMTLSAuth                 `json:"mtls_auth"`
+	ServiceTokenID     string                             `json:"service_token_id"`
+	ServiceTokenStatus *bool                              `json:"service_token_status"`
+	UserUUID           string                             `json:"user_uuid"`
+	Version            int                                `json:"version"`
+}
+
+type AccessUserDevicePostureCheck struct {
+	Exists *bool  `json:"exists"`
+	Path   string `json:"path"`
+}
+
+type AccessUserIdentityGeo struct {
+	Country string `json:"country"`
+}
+
+type AccessUserIDP struct {
+	ID   string `json:"id"`
+	Type string `json:"type"`
+}
+
+type AccessUserMTLSAuth struct {
+	AuthStatus    string `json:"auth_status"`
+	CertIssuerDN  string `json:"cert_issuer_dn"`
+	CertIssuerSKI string `json:"cert_issuer_ski"`
+	CertPresented *bool  `json:"cert_presented"`
+	CertSerial    string `json:"cert_serial"`
+}
+
+type AccessUserListResponse struct {
+	Result     []AccessUser `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+type AccessUser struct {
+	ID                  string `json:"id"`
+	AccessSeat          *bool  `json:"access_seat"`
+	ActiveDeviceCount   int    `json:"active_device_count"`
+	CreatedAt           string `json:"created_at"`
+	Email               string `json:"email"`
+	GatewaySeat         *bool  `json:"gateway_seat"`
+	LastSuccessfulLogin string `json:"last_successful_login"`
+	Name                string `json:"name"`
+	SeatUID             string `json:"seat_uid"`
+	UID                 string `json:"uid"`
+	UpdatedAt           string `json:"updated_at"`
+}
+
+type AccessUserParams struct {
+	ResultInfo
+}
+
+type GetAccessUserSingleActiveSessionResponse struct {
+	Result     GetAccessUserSingleActiveSessionResult `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+type GetAccessUserSingleActiveSessionResult struct {
+	AccountID          string                             `json:"account_id"`
+	AuthStatus         string                             `json:"auth_status"`
+	CommonName         string                             `json:"common_name"`
+	DevicePosture      map[string]AccessUserDevicePosture `json:"devicePosture"`
+	DeviceSessions     map[string]AccessUserDeviceSession `json:"device_sessions"`
+	DeviceID           string                             `json:"device_id"`
+	Email              string                             `json:"email"`
+	Geo                AccessUserIdentityGeo              `json:"geo"`
+	IAT                int64                              `json:"iat"`
+	IDP                AccessUserIDP                      `json:"idp"`
+	IP                 string                             `json:"ip"`
+	IsGateway          *bool                              `json:"is_gateway"`
+	IsWarp             *bool                              `json:"is_warp"`
+	MtlsAuth           AccessUserMTLSAuth                 `json:"mtls_auth"`
+	ServiceTokenID     string                             `json:"service_token_id"`
+	ServiceTokenStatus *bool                              `json:"service_token_status"`
+	UserUUID           string                             `json:"user_uuid"`
+	Version            int                                `json:"version"`
+	IsActive           *bool                              `json:"isActive"`
+}
+
+// ListAccessUsers returns a list of users for a single cloudflare access/zerotrust account.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/zero-trust-users-get-users
+func (api *API) ListAccessUsers(ctx context.Context, rc *ResourceContainer, params AccessUserParams) ([]AccessUser, *ResultInfo, error) {
+	if rc.Level != AccountRouteLevel {
+		return []AccessUser{}, &ResultInfo{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level)
+	}
+
+	baseURL := fmt.Sprintf("/%s/%s/access/users", rc.Level, rc.Identifier)
+
+	autoPaginate := true
+	if params.PerPage >= 1 || params.Page >= 1 {
+		autoPaginate = false
+	}
+
+	if params.PerPage < 1 {
+		params.PerPage = 25
+	}
+
+	if params.Page < 1 {
+		params.Page = 1
+	}
+
+	var accessUsers []AccessUser
+	var r AccessUserListResponse
+
+	for {
+		uri := buildURI(baseURL, params)
+		res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+		if err != nil {
+			return []AccessUser{}, &ResultInfo{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+		}
+
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return []AccessUser{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+		}
+		accessUsers = append(accessUsers, r.Result...)
+		params.ResultInfo = r.ResultInfo.Next()
+		if params.ResultInfo.Done() || !autoPaginate {
+			break
+		}
+	}
+
+	return accessUsers, &r.ResultInfo, nil
+}
+
+// GetAccessUserActiveSessions returns a list of active sessions for an user.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/zero-trust-users-get-active-sessions
+func (api *API) GetAccessUserActiveSessions(ctx context.Context, rc *ResourceContainer, userID string) ([]AccessUserActiveSessionResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return []AccessUserActiveSessionResult{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level)
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/access/users/%s/active_sessions",
+		rc.Level,
+		rc.Identifier,
+		userID,
+	)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []AccessUserActiveSessionResult{}, err
+	}
+
+	var accessUserActiveSessionsResponse AccessUserActiveSessionsResponse
+	err = json.Unmarshal(res, &accessUserActiveSessionsResponse)
+	if err != nil {
+		return []AccessUserActiveSessionResult{}, err
+	}
+	return accessUserActiveSessionsResponse.Result, nil
+}
+
+// GetAccessUserSingleActiveSession returns a single active session for a user.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/zero-trust-users-get-active-session
+func (api *API) GetAccessUserSingleActiveSession(ctx context.Context, rc *ResourceContainer, userID string, sessionID string) (GetAccessUserSingleActiveSessionResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return GetAccessUserSingleActiveSessionResult{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level)
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/access/users/%s/active_sessions/%s",
+		rc.Level,
+		rc.Identifier,
+		userID,
+		sessionID,
+	)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return GetAccessUserSingleActiveSessionResult{}, err
+	}
+
+	var accessUserActiveSingleSessionsResponse GetAccessUserSingleActiveSessionResponse
+	err = json.Unmarshal(res, &accessUserActiveSingleSessionsResponse)
+	if err != nil {
+		return GetAccessUserSingleActiveSessionResult{}, err
+	}
+	return accessUserActiveSingleSessionsResponse.Result, nil
+}
+
+// GetAccessUserFailedLogins returns a list of failed logins for a user.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/zero-trust-users-get-failed-logins
+func (api *API) GetAccessUserFailedLogins(ctx context.Context, rc *ResourceContainer, userID string) ([]AccessUserFailedLoginResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return []AccessUserFailedLoginResult{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level)
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/access/users/%s/failed_logins",
+		rc.Level,
+		rc.Identifier,
+		userID,
+	)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []AccessUserFailedLoginResult{}, err
+	}
+
+	var accessUserFailedLoginsResponse AccessUserFailedLoginsResponse
+	err = json.Unmarshal(res, &accessUserFailedLoginsResponse)
+	if err != nil {
+		return []AccessUserFailedLoginResult{}, err
+	}
+	return accessUserFailedLoginsResponse.Result, nil
+}
+
+// GetAccessUserLastSeenIdentity returns the last seen identity for a user.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/zero-trust-users-get-last-seen-identity
+func (api *API) GetAccessUserLastSeenIdentity(ctx context.Context, rc *ResourceContainer, userID string) (GetAccessUserLastSeenIdentityResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return GetAccessUserLastSeenIdentityResult{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level)
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/access/users/%s/last_seen_identity",
+		rc.Level,
+		rc.Identifier,
+		userID,
+	)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return GetAccessUserLastSeenIdentityResult{}, err
+	}
+
+	var accessUserLastSeenIdentityResponse AccessUserLastSeenIdentitySessionResponse
+	err = json.Unmarshal(res, &accessUserLastSeenIdentityResponse)
+	if err != nil {
+		return GetAccessUserLastSeenIdentityResult{}, err
+	}
+	return accessUserLastSeenIdentityResponse.Result, nil
+}


Test patch:
diff --git a/access_seats_test.go b/access_seats_test.go
new file mode 100644
index 000000000..9b52235f6
--- /dev/null
+++ b/access_seats_test.go
@@ -0,0 +1,82 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var testAccessGroupSeatUID = "access-group-seat-uid"
+
+func TestUpdateAccessUserSeat_ZoneIsNotSupported(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := client.UpdateAccessUserSeat(context.Background(), testZoneRC, UpdateAccessUserSeatParams{})
+	assert.EqualError(t, err, fmt.Sprintf(errInvalidResourceContainerAccess, ZoneRouteLevel))
+}
+
+func TestUpdateAccessUserSeat_MissingUID(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := client.UpdateAccessUserSeat(context.Background(), testAccountRC, UpdateAccessUserSeatParams{})
+	assert.EqualError(t, err, "missing required access seat UID")
+}
+
+func TestUpdateAccessUserSeat(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"errors": [],
+			"messages": [],
+			"result": [
+			  {
+				"access_seat": false,
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"gateway_seat": false,
+				"seat_uid": null,
+				"updated_at": "2014-01-01T05:20:00.12345Z"
+			  }
+			],
+			"success": true,
+			"result_info": {
+			  "count": 1,
+			  "page": 1,
+			  "per_page": 20,
+			  "total_count": 2000
+			}
+		  }
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/seats", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	want := []AccessUpdateAccessUserSeatResult{
+		{
+			AccessSeat:  BoolPtr(false),
+			CreatedAt:   &createdAt,
+			GatewaySeat: BoolPtr(false),
+			SeatUID:     "",
+			UpdatedAt:   &updatedAt,
+		},
+	}
+
+	actual, err := client.UpdateAccessUserSeat(context.Background(), testAccountRC, UpdateAccessUserSeatParams{
+		SeatUID:     testAccessGroupSeatUID,
+		AccessSeat:  BoolPtr(false),
+		GatewaySeat: BoolPtr(false),
+	})
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
diff --git a/access_users_test.go b/access_users_test.go
new file mode 100644
index 000000000..0d2e4bee0
--- /dev/null
+++ b/access_users_test.go
@@ -0,0 +1,573 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	testAccessUserID        = "access-user-id"
+	testAccessUserSessionID = "access-user-session-id"
+
+	expectedListAccessUserResult = AccessUser{
+		AccessSeat:          BoolPtr(false),
+		ActiveDeviceCount:   2,
+		CreatedAt:           "2014-01-01T05:20:00.12345Z",
+		Email:               "jdoe@example.com",
+		GatewaySeat:         BoolPtr(false),
+		ID:                  "f3b12456-80dd-4e89-9f5f-ba3dfff12365",
+		LastSuccessfulLogin: "2020-07-01T05:20:00Z",
+		Name:                "Jane Doe",
+		SeatUID:             "",
+		UID:                 "",
+		UpdatedAt:           "2014-01-01T05:20:00.12345Z",
+	}
+
+	expectedGetAccessUserActiveSessionsResult = AccessUserActiveSessionResult{
+		Expiration: 1694813506,
+		Metadata: AccessUserActiveSessionMetadata{
+			Apps: map[string]AccessUserActiveSessionMetadataApp{
+				"property1": {
+					Hostname: "test.example.com",
+					Name:     "app name",
+					Type:     "self_hosted",
+					UID:      "cc2a8145-0128-4429-87f3-872c4d380c4e",
+				},
+				"property2": {
+					Hostname: "test.example.com",
+					Name:     "app name",
+					Type:     "self_hosted",
+					UID:      "cc2a8145-0128-4429-87f3-872c4d380c4e",
+				},
+			},
+			Expires: 1694813506,
+			IAT:     1694791905,
+			Nonce:   "X1aXj1lFVcqqyoXF",
+			TTL:     21600,
+		},
+		Name: "string",
+	}
+
+	expectedGetAccessUserFailedLoginsResult = AccessUserFailedLoginResult{
+		Expiration: 0,
+		Metadata: AccessUserFailedLoginMetadata{
+			AppName:   "Test App",
+			Aud:       "39691c1480a2352a18ece567debc2b32552686cbd38eec0887aa18d5d3f00c04",
+			Datetime:  "2022-02-02T21:54:34.914Z",
+			RayID:     "6d76a8a42ead4133",
+			UserEmail: "test@cloudflare.com",
+			UserUUID:  "57171132-e453-4ee8-b2a5-8cbaad333207",
+		},
+	}
+
+	expectedGetAccessUserLastSeenIdentityResult = GetAccessUserLastSeenIdentityResult{
+		AccountID:  "1234567890",
+		AuthStatus: "NONE",
+		CommonName: "",
+		DevicePosture: map[string]AccessUserDevicePosture{
+			"property1": {
+				Check: AccessUserDevicePostureCheck{
+					Exists: BoolPtr(true),
+					Path:   "string",
+				},
+				Data:        map[string]interface{}{},
+				Description: "string",
+				Error:       "string",
+				ID:          "string",
+				RuleName:    "string",
+				Success:     BoolPtr(true),
+				Timestamp:   "string",
+				Type:        "string",
+			},
+			"property2": {
+				Check: AccessUserDevicePostureCheck{
+					Exists: BoolPtr(true),
+					Path:   "string",
+				},
+				Data:        map[string]interface{}{},
+				Description: "string",
+				Error:       "string",
+				ID:          "string",
+				RuleName:    "string",
+				Success:     BoolPtr(true),
+				Timestamp:   "string",
+				Type:        "string",
+			},
+		},
+		DeviceID: "",
+		DeviceSessions: map[string]AccessUserDeviceSession{
+			"property1": {
+				LastAuthenticated: 1638832687,
+			},
+			"property2": {
+				LastAuthenticated: 1638832687,
+			},
+		},
+		Email: "test@cloudflare.com",
+		Geo: AccessUserIdentityGeo{
+			Country: "US",
+		},
+		IAT: 1694791905,
+		IDP: AccessUserIDP{
+			ID:   "string",
+			Type: "string",
+		},
+		IP:        "127.0.0.0",
+		IsGateway: BoolPtr(false),
+		IsWarp:    BoolPtr(false),
+		MtlsAuth: AccessUserMTLSAuth{
+			AuthStatus:    "string",
+			CertIssuerDN:  "string",
+			CertIssuerSKI: "string",
+			CertPresented: BoolPtr(true),
+			CertSerial:    "string",
+		},
+		ServiceTokenID:     "",
+		ServiceTokenStatus: BoolPtr(false),
+		UserUUID:           "57cf8cf2-f55a-4588-9ac9-f5e41e9f09b4",
+		Version:            2,
+	}
+
+	expectedGetAccessUserSingleActiveSessionResult = GetAccessUserSingleActiveSessionResult{
+		AccountID:  "1234567890",
+		AuthStatus: "NONE",
+		CommonName: "",
+		DevicePosture: map[string]AccessUserDevicePosture{
+			"property1": {
+				Check: AccessUserDevicePostureCheck{
+					Exists: BoolPtr(true),
+					Path:   "string",
+				},
+				Data:        map[string]interface{}{},
+				Description: "string",
+				Error:       "string",
+				ID:          "string",
+				RuleName:    "string",
+				Success:     BoolPtr(true),
+				Timestamp:   "string",
+				Type:        "string",
+			},
+			"property2": {
+				Check: AccessUserDevicePostureCheck{
+					Exists: BoolPtr(true),
+					Path:   "string",
+				},
+				Data:        map[string]interface{}{},
+				Description: "string",
+				Error:       "string",
+				ID:          "string",
+				RuleName:    "string",
+				Success:     BoolPtr(true),
+				Timestamp:   "string",
+				Type:        "string",
+			},
+		},
+		DeviceID: "",
+		DeviceSessions: map[string]AccessUserDeviceSession{
+			"property1": {
+				LastAuthenticated: 1638832687,
+			},
+			"property2": {
+				LastAuthenticated: 1638832687,
+			},
+		},
+		Email: "test@cloudflare.com",
+		Geo: AccessUserIdentityGeo{
+			Country: "US",
+		},
+		IAT: 1694791905,
+		IDP: AccessUserIDP{
+			ID:   "string",
+			Type: "string",
+		},
+		IP:        "127.0.0.0",
+		IsGateway: BoolPtr(false),
+		IsWarp:    BoolPtr(false),
+		MtlsAuth: AccessUserMTLSAuth{
+			AuthStatus:    "string",
+			CertIssuerDN:  "string",
+			CertIssuerSKI: "string",
+			CertPresented: BoolPtr(true),
+			CertSerial:    "string",
+		},
+		ServiceTokenID:     "",
+		ServiceTokenStatus: BoolPtr(false),
+		UserUUID:           "57cf8cf2-f55a-4588-9ac9-f5e41e9f09b4",
+		Version:            2,
+		IsActive:           BoolPtr(true),
+	}
+)
+
+func TestListAccessUsers_ZoneIsNotSupported(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, _, err := client.ListAccessUsers(context.Background(), testZoneRC, AccessUserParams{})
+	assert.EqualError(t, err, fmt.Sprintf(errInvalidResourceContainerAccess, ZoneRouteLevel))
+}
+
+func TestListAccessUsers(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"errors": [],
+			"messages": [],
+			"result": [
+			  {
+				"access_seat": false,
+				"active_device_count": 2,
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"email": "jdoe@example.com",
+				"gateway_seat": false,
+				"id": "f3b12456-80dd-4e89-9f5f-ba3dfff12365",
+				"last_successful_login": "2020-07-01T05:20:00Z",
+				"name": "Jane Doe",
+				"seat_uid": null,
+				"uid": null,
+				"updated_at": "2014-01-01T05:20:00.12345Z"
+			  }
+			],
+			"success": true,
+			"result_info": {
+			  "count": 1,
+			  "page": 1,
+			  "per_page": 100,
+			  "total_count": 1
+			}
+		  }
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/users", handler)
+
+	actual, _, err := client.ListAccessUsers(context.Background(), testAccountRC, AccessUserParams{})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, []AccessUser{expectedListAccessUserResult}, actual)
+	}
+}
+
+func TestGetGetAccessUserActiveSessions_ZoneIsNotSupported(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := client.GetAccessUserActiveSessions(context.Background(), testZoneRC, testAccessUserID)
+	assert.EqualError(t, err, fmt.Sprintf(errInvalidResourceContainerAccess, ZoneRouteLevel))
+}
+
+func TestGetGetAccessUserActiveSessions(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"errors": [],
+			"messages": [],
+			"result": [
+			  {
+				"expiration": 1694813506,
+				"metadata": {
+				  "apps": {
+					"property1": {
+					  "hostname": "test.example.com",
+					  "name": "app name",
+					  "type": "self_hosted",
+					  "uid": "cc2a8145-0128-4429-87f3-872c4d380c4e"
+					},
+					"property2": {
+					  "hostname": "test.example.com",
+					  "name": "app name",
+					  "type": "self_hosted",
+					  "uid": "cc2a8145-0128-4429-87f3-872c4d380c4e"
+					}
+				  },
+				  "expires": 1694813506,
+				  "iat": 1694791905,
+				  "nonce": "X1aXj1lFVcqqyoXF",
+				  "ttl": 21600
+				},
+				"name": "string"
+			  }
+			],
+			"success": true,
+			"result_info": {
+			  "count": 1,
+			  "page": 1,
+			  "per_page": 20,
+			  "total_count": 2000
+			}
+		  }
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/users/"+testAccessUserID+"/active_sessions", handler)
+
+	actual, err := client.GetAccessUserActiveSessions(context.Background(), testAccountRC, testAccessUserID)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, []AccessUserActiveSessionResult{expectedGetAccessUserActiveSessionsResult}, actual)
+	}
+}
+
+func TestGetAccessUserSingleActiveSession_ZoneIsNotSupported(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := client.GetAccessUserSingleActiveSession(context.Background(), testZoneRC, testAccessUserID, testAccessUserSessionID)
+	assert.EqualError(t, err, fmt.Sprintf(errInvalidResourceContainerAccess, ZoneRouteLevel))
+}
+
+func TestGetAccessUserSingleActiveSession(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"errors": [],
+			"messages": [],
+			"result": {
+			  "account_id": "1234567890",
+			  "auth_status": "NONE",
+			  "common_name": "",
+			  "devicePosture": {
+				"property1": {
+				  "check": {
+					"exists": true,
+					"path": "string"
+				  },
+				  "data": {},
+				  "description": "string",
+				  "error": "string",
+				  "id": "string",
+				  "rule_name": "string",
+				  "success": true,
+				  "timestamp": "string",
+				  "type": "string"
+				},
+				"property2": {
+				  "check": {
+					"exists": true,
+					"path": "string"
+				  },
+				  "data": {},
+				  "description": "string",
+				  "error": "string",
+				  "id": "string",
+				  "rule_name": "string",
+				  "success": true,
+				  "timestamp": "string",
+				  "type": "string"
+				}
+			  },
+			  "device_id": "",
+			  "device_sessions": {
+				"property1": {
+				  "last_authenticated": 1638832687
+				},
+				"property2": {
+				  "last_authenticated": 1638832687
+				}
+			  },
+			  "email": "test@cloudflare.com",
+			  "geo": {
+				"country": "US"
+			  },
+			  "iat": 1694791905,
+			  "idp": {
+				"id": "string",
+				"type": "string"
+			  },
+			  "ip": "127.0.0.0",
+			  "is_gateway": false,
+			  "is_warp": false,
+			  "mtls_auth": {
+				"auth_status": "string",
+				"cert_issuer_dn": "string",
+				"cert_issuer_ski": "string",
+				"cert_presented": true,
+				"cert_serial": "string"
+			  },
+			  "service_token_id": "",
+			  "service_token_status": false,
+			  "user_uuid": "57cf8cf2-f55a-4588-9ac9-f5e41e9f09b4",
+			  "version": 2,
+			  "isActive": true
+			},
+			"success": true
+		  }
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/users/"+testAccessUserID+"/active_sessions/"+testAccessUserSessionID, handler)
+
+	actual, err := client.GetAccessUserSingleActiveSession(context.Background(), testAccountRC, testAccessUserID, testAccessUserSessionID)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedGetAccessUserSingleActiveSessionResult, actual)
+	}
+}
+
+func TestGetAccessUserFailedLogins_ZoneIsNotSupported(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := client.GetAccessUserFailedLogins(context.Background(), testZoneRC, testAccessUserID)
+	assert.EqualError(t, err, fmt.Sprintf(errInvalidResourceContainerAccess, ZoneRouteLevel))
+}
+
+func TestGetAccessUserFailedLogins(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"errors": [],
+			"messages": [],
+			"result": [
+			  {
+				"expiration": 0,
+				"metadata": {
+				  "app_name": "Test App",
+				  "aud": "39691c1480a2352a18ece567debc2b32552686cbd38eec0887aa18d5d3f00c04",
+				  "datetime": "2022-02-02T21:54:34.914Z",
+				  "ray_id": "6d76a8a42ead4133",
+				  "user_email": "test@cloudflare.com",
+				  "user_uuid": "57171132-e453-4ee8-b2a5-8cbaad333207"
+				}
+			  }
+			],
+			"success": true,
+			"result_info": {
+			  "count": 1,
+			  "page": 1,
+			  "per_page": 20,
+			  "total_count": 2000
+			}
+		  }
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/users/"+testAccessUserID+"/failed_logins", handler)
+
+	actual, err := client.GetAccessUserFailedLogins(context.Background(), testAccountRC, testAccessUserID)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, []AccessUserFailedLoginResult{expectedGetAccessUserFailedLoginsResult}, actual)
+	}
+}
+
+func TestGetAccessUserLastSeenIdentity_ZoneIsNotSupported(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := client.GetAccessUserLastSeenIdentity(context.Background(), testZoneRC, testAccessUserID)
+	assert.EqualError(t, err, fmt.Sprintf(errInvalidResourceContainerAccess, ZoneRouteLevel))
+}
+
+func TestGetAccessUserLastSeenIdentity(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+  "errors": [],
+  "messages": [],
+  "result": {
+    "account_id": "1234567890",
+    "auth_status": "NONE",
+    "common_name": "",
+    "devicePosture": {
+      "property1": {
+        "check": {
+          "exists": true,
+          "path": "string"
+        },
+        "data": {},
+        "description": "string",
+        "error": "string",
+        "id": "string",
+        "rule_name": "string",
+        "success": true,
+        "timestamp": "string",
+        "type": "string"
+      },
+      "property2": {
+        "check": {
+          "exists": true,
+          "path": "string"
+        },
+        "data": {},
+        "description": "string",
+        "error": "string",
+        "id": "string",
+        "rule_name": "string",
+        "success": true,
+        "timestamp": "string",
+        "type": "string"
+      }
+    },
+    "device_id": "",
+    "device_sessions": {
+      "property1": {
+        "last_authenticated": 1638832687
+      },
+      "property2": {
+        "last_authenticated": 1638832687
+      }
+    },
+    "email": "test@cloudflare.com",
+    "geo": {
+      "country": "US"
+    },
+    "iat": 1694791905,
+    "idp": {
+      "id": "string",
+      "type": "string"
+    },
+    "ip": "127.0.0.0",
+    "is_gateway": false,
+    "is_warp": false,
+    "mtls_auth": {
+      "auth_status": "string",
+      "cert_issuer_dn": "string",
+      "cert_issuer_ski": "string",
+      "cert_presented": true,
+      "cert_serial": "string"
+    },
+    "service_token_id": "",
+    "service_token_status": false,
+    "user_uuid": "57cf8cf2-f55a-4588-9ac9-f5e41e9f09b4",
+    "version": 2
+  },
+  "success": true
+}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/users/"+testAccessUserID+"/last_seen_identity", handler)
+
+	actual, err := client.GetAccessUserLastSeenIdentity(context.Background(), testAccountRC, testAccessUserID)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedGetAccessUserLastSeenIdentityResult, actual)
+	}
+}