Skip to content

Commit 931083b

Browse files
committed
v2: added UsersResource
Updates tailscale/corp#21629 Signed-off-by: Percy Wegmann <[email protected]>
1 parent 6e74358 commit 931083b

File tree

4 files changed

+199
-0
lines changed

4 files changed

+199
-0
lines changed

v2/client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type (
4545
keys *KeysResource
4646
logging *LoggingResource
4747
policyFile *PolicyFileResource
48+
users *UsersResource
4849
webhooks *WebhooksResource
4950
}
5051

@@ -104,6 +105,7 @@ func (c *Client) init() {
104105
c.keys = &KeysResource{c}
105106
c.logging = &LoggingResource{c}
106107
c.policyFile = &PolicyFileResource{c}
108+
c.users = &UsersResource{c}
107109
c.webhooks = &WebhooksResource{c}
108110
})
109111
}
@@ -157,6 +159,11 @@ func (c *Client) PolicyFile() *PolicyFileResource {
157159
return c.policyFile
158160
}
159161

162+
func (c *Client) Users() *UsersResource {
163+
c.init()
164+
return c.users
165+
}
166+
160167
func (c *Client) Webhooks() *WebhooksResource {
161168
c.init()
162169
return c.webhooks

v2/tailscale_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type TestServer struct {
2121

2222
Method string
2323
Path string
24+
Query url.Values
2425
Body *bytes.Buffer
2526
Header http.Header
2627

@@ -69,6 +70,7 @@ func NewTestHarness(t *testing.T) (*tsclient.Client, *TestServer) {
6970
func (t *TestServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7071
t.Method = r.Method
7172
t.Path = r.URL.Path
73+
t.Query = r.URL.Query()
7274
t.Header = r.Header
7375

7476
t.Body = bytes.NewBuffer([]byte{})

v2/users.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package tsclient
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"time"
7+
)
8+
9+
const (
10+
UserTypeMember UserType = "member"
11+
UserTypeShared UserType = "shared"
12+
)
13+
14+
const (
15+
UserRoleOwner UserRole = "owner"
16+
UserRoleMember UserRole = "member"
17+
UserRoleAdmin UserRole = "admin"
18+
UserRoleITAdmin UserRole = "it-admin"
19+
UserRoleNetworkAdmin UserRole = "network-admin"
20+
UserRoleBillingAdmin UserRole = "billing-admin"
21+
UserRoleAuditor UserRole = "auditor"
22+
)
23+
24+
const (
25+
UserStatusActive UserStatus = "active"
26+
UserStatusIdle UserStatus = "idle"
27+
UserStatusSuspended UserStatus = "suspended"
28+
UserStatusNeedsApproval UserStatus = "needs-approval"
29+
UserStatusOverBillingLimit UserStatus = "over-billing-limit"
30+
)
31+
32+
type (
33+
// UserType is the type of relation this user has to the tailnet associated with the request.
34+
UserType string
35+
36+
// UserRole is the role of the user.
37+
UserRole string
38+
39+
// UserStatus is the status of the user.
40+
UserStatus string
41+
42+
// User is a representation of a user within a tailnet.
43+
User struct {
44+
ID string `json:"id"`
45+
DisplayName string `json:"displayName"`
46+
LoginName string `json:"loginName"`
47+
ProfilePicURL string `json:"profilePicUrl"`
48+
TailnetID string `json:"tailnetId"`
49+
Created time.Time `json:"created"`
50+
Type UserType `json:"type"`
51+
Role UserRole `json:"role"`
52+
Status UserStatus `json:"status"`
53+
DeviceCount int `json:"deviceCount"`
54+
LastSeen time.Time `json:"lastSeen"`
55+
CurrentlyConnected bool `json:"currentlyConnected"`
56+
}
57+
)
58+
59+
type UsersResource struct {
60+
*Client
61+
}
62+
63+
// List lists all [User]s of a tailnet. If userType and/or role are provided,
64+
// the list of users will be filtered by those.
65+
func (ur *UsersResource) List(ctx context.Context, userType *UserType, role *UserRole) ([]User, error) {
66+
u := ur.buildTailnetURL("users")
67+
q := u.Query()
68+
if userType != nil {
69+
q.Add("type", string(*userType))
70+
}
71+
if role != nil {
72+
q.Add("role", string(*role))
73+
}
74+
u.RawQuery = q.Encode()
75+
76+
req, err := ur.buildRequest(ctx, http.MethodGet, u)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
resp := make(map[string][]User)
82+
if err = ur.do(req, &resp); err != nil {
83+
return nil, err
84+
}
85+
86+
return resp["users"], nil
87+
}
88+
89+
// Get retrieves the [User] identified by the given id.
90+
func (ur *UsersResource) Get(ctx context.Context, id string) (*User, error) {
91+
req, err := ur.buildRequest(ctx, http.MethodGet, ur.buildURL("users", id))
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
var resp User
97+
return &resp, ur.do(req, &resp)
98+
}

v2/users_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package tsclient_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
tsclient "github.com/tailscale/tailscale-client-go/v2"
12+
)
13+
14+
func TestClient_Users_List(t *testing.T) {
15+
t.Parallel()
16+
17+
client, server := NewTestHarness(t)
18+
server.ResponseCode = http.StatusOK
19+
20+
expectedUsers := map[string][]tsclient.User{
21+
"users": {
22+
{
23+
ID: "12345",
24+
DisplayName: "Jane Doe",
25+
LoginName: "janedoe",
26+
ProfilePicURL: "http://example.com/users/janedoe",
27+
TailnetID: "1",
28+
Created: time.Date(2022, 2, 10, 11, 50, 23, 0, time.UTC),
29+
Type: tsclient.UserTypeMember,
30+
Role: tsclient.UserRoleOwner,
31+
Status: tsclient.UserStatusActive,
32+
DeviceCount: 2,
33+
LastSeen: time.Date(2022, 2, 10, 12, 50, 23, 0, time.UTC),
34+
CurrentlyConnected: true,
35+
},
36+
{
37+
ID: "12346",
38+
DisplayName: "John Doe",
39+
LoginName: "johndoe",
40+
ProfilePicURL: "http://example.com/users/johndoe",
41+
TailnetID: "2",
42+
Created: time.Date(2022, 2, 10, 11, 50, 23, 12, time.UTC),
43+
Type: tsclient.UserTypeShared,
44+
Role: tsclient.UserRoleMember,
45+
Status: tsclient.UserStatusIdle,
46+
DeviceCount: 2,
47+
LastSeen: time.Date(2022, 2, 10, 12, 50, 23, 12, time.UTC),
48+
CurrentlyConnected: true,
49+
},
50+
},
51+
}
52+
server.ResponseBody = expectedUsers
53+
54+
actualUsers, err := client.Users().List(
55+
context.Background(),
56+
tsclient.PointerTo(tsclient.UserTypeMember),
57+
tsclient.PointerTo(tsclient.UserRoleAdmin))
58+
assert.NoError(t, err)
59+
assert.Equal(t, http.MethodGet, server.Method)
60+
assert.Equal(t, "/api/v2/tailnet/example.com/users", server.Path)
61+
assert.Equal(t, url.Values{"type": []string{"member"}, "role": []string{"admin"}}, server.Query)
62+
assert.Equal(t, expectedUsers["users"], actualUsers)
63+
}
64+
65+
func TestClient_Users_Get(t *testing.T) {
66+
t.Parallel()
67+
68+
client, server := NewTestHarness(t)
69+
server.ResponseCode = http.StatusOK
70+
71+
expectedUser := &tsclient.User{
72+
ID: "12345",
73+
DisplayName: "Jane Doe",
74+
LoginName: "janedoe",
75+
ProfilePicURL: "http://example.com/users/janedoe",
76+
TailnetID: "1",
77+
Created: time.Date(2022, 2, 10, 11, 50, 23, 0, time.UTC),
78+
Type: tsclient.UserTypeMember,
79+
Role: tsclient.UserRoleOwner,
80+
Status: tsclient.UserStatusActive,
81+
DeviceCount: 2,
82+
LastSeen: time.Date(2022, 2, 10, 12, 50, 23, 0, time.UTC),
83+
CurrentlyConnected: true,
84+
}
85+
server.ResponseBody = expectedUser
86+
87+
actualUser, err := client.Users().Get(context.Background(), "12345")
88+
assert.NoError(t, err)
89+
assert.Equal(t, http.MethodGet, server.Method)
90+
assert.Equal(t, "/api/v2/users/12345", server.Path)
91+
assert.Equal(t, expectedUser, actualUser)
92+
}

0 commit comments

Comments
 (0)