1package admin
2
3import (
4	"encoding/json"
5	"fmt"
6	"net/http"
7	"net/url"
8	"strconv"
9
10	"github.com/duosecurity/duo_api_golang"
11)
12
13// Client provides access to Duo's admin API.
14type Client struct {
15	duoapi.DuoApi
16}
17
18type ListResultMetadata struct {
19	NextOffset   json.Number `json:"next_offset"`
20	PrevOffset   json.Number `json:"prev_offset"`
21	TotalObjects json.Number `json:"total_objects"`
22}
23
24type ListResult struct {
25	Metadata ListResultMetadata `json:"metadata"`
26}
27
28func (l *ListResult) metadata() ListResultMetadata {
29	return l.Metadata
30}
31
32// New initializes an admin API Client struct.
33func New(base duoapi.DuoApi) *Client {
34	return &Client{base}
35}
36
37// User models a single user.
38type User struct {
39	Alias1            *string
40	Alias2            *string
41	Alias3            *string
42	Alias4            *string
43	Created           uint64
44	Email             string
45	FirstName         *string
46	Groups            []Group
47	LastDirectorySync *uint64 `json:"last_directory_sync"`
48	LastLogin         *uint64 `json:"last_login"`
49	LastName          *string
50	Notes             string
51	Phones            []Phone
52	RealName          *string
53	Status            string
54	Tokens            []Token
55	UserID            string `json:"user_id"`
56	Username          string
57}
58
59// Group models a group to which users may belong.
60type Group struct {
61	Desc             string
62	GroupID          string `json:"group_id"`
63	MobileOTPEnabled bool   `json:"mobile_otp_enabled"`
64	Name             string
65	PushEnabled      bool `json:"push_enabled"`
66	SMSEnabled       bool `json:"sms_enabled"`
67	Status           string
68	VoiceEnabled     bool `json:"voice_enabled"`
69}
70
71// Phone models a user's phone.
72type Phone struct {
73	Activated        bool
74	Capabilities     []string
75	Encrypted        string
76	Extension        string
77	Fingerprint      string
78	Name             string
79	Number           string
80	PhoneID          string `json:"phone_id"`
81	Platform         string
82	Postdelay        string
83	Predelay         string
84	Screenlock       string
85	SMSPasscodesSent bool
86	Type             string
87	Users            []User
88}
89
90// Token models a hardware security token.
91type Token struct {
92	TokenID  string `json:"token_id"`
93	Type     string
94	Serial   string
95	TOTPStep *int `json:"totp_step"`
96	Users    []User
97}
98
99// U2FToken models a U2F security token.
100type U2FToken struct {
101	DateAdded      uint64 `json:"date_added"`
102	RegistrationID string `json:"registration_id"`
103	User           *User
104}
105
106// Common URL options
107
108// Limit sets the optional limit parameter for an API request.
109func Limit(limit uint64) func(*url.Values) {
110	return func(opts *url.Values) {
111		opts.Set("limit", strconv.FormatUint(limit, 10))
112	}
113}
114
115// Offset sets the optional offset parameter for an API request.
116func Offset(offset uint64) func(*url.Values) {
117	return func(opts *url.Values) {
118		opts.Set("offset", strconv.FormatUint(offset, 10))
119	}
120}
121
122// User methods
123
124// GetUsersUsername sets the optional username parameter for a GetUsers request.
125func GetUsersUsername(name string) func(*url.Values) {
126	return func(opts *url.Values) {
127		opts.Set("username", name)
128	}
129}
130
131// GetUsersResult models responses containing a list of users.
132type GetUsersResult struct {
133	duoapi.StatResult
134	ListResult
135	Response []User
136}
137
138// GetUserResult models responses containing a single user.
139type GetUserResult struct {
140	duoapi.StatResult
141	Response User
142}
143
144func (result *GetUsersResult) getResponse() interface{} {
145	return result.Response
146}
147
148func (result *GetUsersResult) appendResponse(users interface{}) {
149	asserted_users := users.([]User)
150	result.Response = append(result.Response, asserted_users...)
151}
152
153// GetUsers calls GET /admin/v1/users
154// See https://duo.com/docs/adminapi#retrieve-users
155func (c *Client) GetUsers(options ...func(*url.Values)) (*GetUsersResult, error) {
156	params := url.Values{}
157	for _, o := range options {
158		o(&params)
159	}
160
161	cb := func(params url.Values) (responsePage, error) {
162		return c.retrieveUsers(params)
163	}
164	response, err := c.retrieveItems(params, cb)
165	if err != nil {
166		return nil, err
167	}
168
169	return response.(*GetUsersResult), nil
170}
171
172type responsePage interface {
173	metadata() ListResultMetadata
174	getResponse() interface{}
175	appendResponse(interface{})
176}
177
178type pageFetcher func(params url.Values) (responsePage, error)
179
180func (c *Client) retrieveItems(
181	params url.Values,
182	fetcher pageFetcher,
183) (responsePage, error) {
184	if params.Get("offset") == "" {
185		params.Set("offset", "0")
186	}
187
188	if params.Get("limit") == "" {
189		params.Set("limit", "100")
190		accumulator, firstErr := fetcher(params)
191
192		if firstErr != nil {
193			return nil, firstErr
194		}
195
196		params.Set("offset", accumulator.metadata().NextOffset.String())
197		for params.Get("offset") != "" {
198			nextResult, err := fetcher(params)
199			if err != nil {
200				return nil, err
201			}
202			nextResult.appendResponse(accumulator.getResponse())
203			accumulator = nextResult
204			params.Set("offset", accumulator.metadata().NextOffset.String())
205		}
206		return accumulator, nil
207	}
208
209	return fetcher(params)
210}
211
212func (c *Client) retrieveUsers(params url.Values) (*GetUsersResult, error) {
213	_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/users", params, duoapi.UseTimeout)
214	if err != nil {
215		return nil, err
216	}
217
218	result := &GetUsersResult{}
219	err = json.Unmarshal(body, result)
220	if err != nil {
221		return nil, err
222	}
223	return result, nil
224}
225
226// GetUser calls GET /admin/v1/users/:user_id
227// See https://duo.com/docs/adminapi#retrieve-user-by-id
228func (c *Client) GetUser(userID string) (*GetUserResult, error) {
229	path := fmt.Sprintf("/admin/v1/users/%s", userID)
230
231	_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
232	if err != nil {
233		return nil, err
234	}
235
236	result := &GetUserResult{}
237	err = json.Unmarshal(body, result)
238	if err != nil {
239		return nil, err
240	}
241	return result, nil
242}
243
244// GetUserGroups calls GET /admin/v1/users/:user_id/groups
245// See https://duo.com/docs/adminapi#retrieve-groups-by-user-id
246func (c *Client) GetUserGroups(userID string, options ...func(*url.Values)) (*GetGroupsResult, error) {
247	params := url.Values{}
248	for _, o := range options {
249		o(&params)
250	}
251
252	cb := func(params url.Values) (responsePage, error) {
253		return c.retrieveUserGroups(userID, params)
254	}
255	response, err := c.retrieveItems(params, cb)
256	if err != nil {
257		return nil, err
258	}
259
260	return response.(*GetGroupsResult), nil
261}
262
263func (c *Client) retrieveUserGroups(userID string, params url.Values) (*GetGroupsResult, error) {
264	path := fmt.Sprintf("/admin/v1/users/%s/groups", userID)
265
266	_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
267	if err != nil {
268		return nil, err
269	}
270
271	result := &GetGroupsResult{}
272	err = json.Unmarshal(body, result)
273	if err != nil {
274		return nil, err
275	}
276	return result, nil
277}
278
279// GetUserPhones calls GET /admin/v1/users/:user_id/phones
280// See https://duo.com/docs/adminapi#retrieve-phones-by-user-id
281func (c *Client) GetUserPhones(userID string, options ...func(*url.Values)) (*GetPhonesResult, error) {
282	params := url.Values{}
283	for _, o := range options {
284		o(&params)
285	}
286
287	cb := func(params url.Values) (responsePage, error) {
288		return c.retrieveUserPhones(userID, params)
289	}
290	response, err := c.retrieveItems(params, cb)
291	if err != nil {
292		return nil, err
293	}
294
295	return response.(*GetPhonesResult), nil
296}
297
298func (c *Client) retrieveUserPhones(userID string, params url.Values) (*GetPhonesResult, error) {
299	path := fmt.Sprintf("/admin/v1/users/%s/phones", userID)
300
301	_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
302	if err != nil {
303		return nil, err
304	}
305
306	result := &GetPhonesResult{}
307	err = json.Unmarshal(body, result)
308	if err != nil {
309		return nil, err
310	}
311	return result, nil
312}
313
314// GetUserTokens calls GET /admin/v1/users/:user_id/tokens
315// See https://duo.com/docs/adminapi#retrieve-hardware-tokens-by-user-id
316func (c *Client) GetUserTokens(userID string, options ...func(*url.Values)) (*GetTokensResult, error) {
317	params := url.Values{}
318	for _, o := range options {
319		o(&params)
320	}
321
322	cb := func(params url.Values) (responsePage, error) {
323		return c.retrieveUserTokens(userID, params)
324	}
325	response, err := c.retrieveItems(params, cb)
326	if err != nil {
327		return nil, err
328	}
329
330	return response.(*GetTokensResult), nil
331}
332
333func (c *Client) retrieveUserTokens(userID string, params url.Values) (*GetTokensResult, error) {
334	path := fmt.Sprintf("/admin/v1/users/%s/tokens", userID)
335
336	_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
337	if err != nil {
338		return nil, err
339	}
340
341	result := &GetTokensResult{}
342	err = json.Unmarshal(body, result)
343	if err != nil {
344		return nil, err
345	}
346	return result, nil
347}
348
349// StringResult models responses containing a simple string.
350type StringResult struct {
351	duoapi.StatResult
352	Response string
353}
354
355// AssociateUserToken calls POST /admin/v1/users/:user_id/tokens
356// See https://duo.com/docs/adminapi#associate-hardware-token-with-user
357func (c *Client) AssociateUserToken(userID, tokenID string) (*StringResult, error) {
358	path := fmt.Sprintf("/admin/v1/users/%s/tokens", userID)
359
360	params := url.Values{}
361	params.Set("token_id", tokenID)
362
363	_, body, err := c.SignedCall(http.MethodPost, path, params, duoapi.UseTimeout)
364	if err != nil {
365		return nil, err
366	}
367
368	result := &StringResult{}
369	err = json.Unmarshal(body, result)
370	if err != nil {
371		return nil, err
372	}
373	return result, nil
374}
375
376// GetUserU2FTokens calls GET /admin/v1/users/:user_id/u2ftokens
377// See https://duo.com/docs/adminapi#retrieve-u2f-tokens-by-user-id
378func (c *Client) GetUserU2FTokens(userID string, options ...func(*url.Values)) (*GetU2FTokensResult, error) {
379	params := url.Values{}
380	for _, o := range options {
381		o(&params)
382	}
383
384	cb := func(params url.Values) (responsePage, error) {
385		return c.retrieveUserU2FTokens(userID, params)
386	}
387	response, err := c.retrieveItems(params, cb)
388	if err != nil {
389		return nil, err
390	}
391
392	return response.(*GetU2FTokensResult), nil
393}
394
395func (c *Client) retrieveUserU2FTokens(userID string, params url.Values) (*GetU2FTokensResult, error) {
396	path := fmt.Sprintf("/admin/v1/users/%s/u2ftokens", userID)
397
398	_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
399	if err != nil {
400		return nil, err
401	}
402
403	result := &GetU2FTokensResult{}
404	err = json.Unmarshal(body, result)
405	if err != nil {
406		return nil, err
407	}
408	return result, nil
409}
410
411// Group methods
412
413// GetGroupsResult models responses containing a list of groups.
414type GetGroupsResult struct {
415	duoapi.StatResult
416	ListResult
417	Response []Group
418}
419
420func (result *GetGroupsResult) getResponse() interface{} {
421	return result.Response
422}
423
424func (result *GetGroupsResult) appendResponse(groups interface{}) {
425	asserted_groups := groups.([]Group)
426	result.Response = append(result.Response, asserted_groups...)
427}
428
429// GetGroups calls GET /admin/v1/groups
430// See https://duo.com/docs/adminapi#retrieve-groups
431func (c *Client) GetGroups(options ...func(*url.Values)) (*GetGroupsResult, error) {
432	params := url.Values{}
433	for _, o := range options {
434		o(&params)
435	}
436
437	cb := func(params url.Values) (responsePage, error) {
438		return c.retrieveGroups(params)
439	}
440	response, err := c.retrieveItems(params, cb)
441	if err != nil {
442		return nil, err
443	}
444
445	return response.(*GetGroupsResult), nil
446}
447
448func (c *Client) retrieveGroups(params url.Values) (*GetGroupsResult, error) {
449	_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/groups", params, duoapi.UseTimeout)
450	if err != nil {
451		return nil, err
452	}
453
454	result := &GetGroupsResult{}
455	err = json.Unmarshal(body, result)
456	if err != nil {
457		return nil, err
458	}
459	return result, nil
460}
461
462// GetGroupResult models responses containing a single group.
463type GetGroupResult struct {
464	duoapi.StatResult
465	Response Group
466}
467
468// GetGroup calls GET /admin/v2/group/:group_id
469// See https://duo.com/docs/adminapi#get-group-info
470func (c *Client) GetGroup(groupID string) (*GetGroupResult, error) {
471	path := fmt.Sprintf("/admin/v2/groups/%s", groupID)
472
473	_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
474	if err != nil {
475		return nil, err
476	}
477
478	result := &GetGroupResult{}
479	err = json.Unmarshal(body, result)
480	if err != nil {
481		return nil, err
482	}
483	return result, nil
484}
485
486// Phone methods
487
488// GetPhonesNumber sets the optional number parameter for a GetPhones request.
489func GetPhonesNumber(number string) func(*url.Values) {
490	return func(opts *url.Values) {
491		opts.Set("number", number)
492	}
493}
494
495// GetPhonesExtension sets the optional extension parameter for a GetPhones request.
496func GetPhonesExtension(ext string) func(*url.Values) {
497	return func(opts *url.Values) {
498		opts.Set("extension", ext)
499	}
500}
501
502// GetPhonesResult models responses containing a list of phones.
503type GetPhonesResult struct {
504	duoapi.StatResult
505	ListResult
506	Response []Phone
507}
508
509func (result *GetPhonesResult) getResponse() interface{} {
510	return result.Response
511}
512
513func (result *GetPhonesResult) appendResponse(phones interface{}) {
514	asserted_phones := phones.([]Phone)
515	result.Response = append(result.Response, asserted_phones...)
516}
517
518// GetPhones calls GET /admin/v1/phones
519// See https://duo.com/docs/adminapi#phones
520func (c *Client) GetPhones(options ...func(*url.Values)) (*GetPhonesResult, error) {
521	params := url.Values{}
522	for _, o := range options {
523		o(&params)
524	}
525
526	cb := func(params url.Values) (responsePage, error) {
527		return c.retrievePhones(params)
528	}
529	response, err := c.retrieveItems(params, cb)
530	if err != nil {
531		return nil, err
532	}
533
534	return response.(*GetPhonesResult), nil
535}
536
537func (c *Client) retrievePhones(params url.Values) (*GetPhonesResult, error) {
538	_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/phones", params, duoapi.UseTimeout)
539	if err != nil {
540		return nil, err
541	}
542
543	result := &GetPhonesResult{}
544	err = json.Unmarshal(body, result)
545	if err != nil {
546		return nil, err
547	}
548	return result, nil
549}
550
551// GetPhoneResult models responses containing a single phone.
552type GetPhoneResult struct {
553	duoapi.StatResult
554	Response Phone
555}
556
557// GetPhone calls GET /admin/v1/phones/:phone_id
558// See https://duo.com/docs/adminapi#retrieve-phone-by-id
559func (c *Client) GetPhone(phoneID string) (*GetPhoneResult, error) {
560	path := fmt.Sprintf("/admin/v1/phones/%s", phoneID)
561
562	_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
563	if err != nil {
564		return nil, err
565	}
566
567	result := &GetPhoneResult{}
568	err = json.Unmarshal(body, result)
569	if err != nil {
570		return nil, err
571	}
572	return result, nil
573}
574
575// Token methods
576
577// GetTokensTypeAndSerial sets the optional type and serial parameters for a GetTokens request.
578func GetTokensTypeAndSerial(typ, serial string) func(*url.Values) {
579	return func(opts *url.Values) {
580		opts.Set("type", typ)
581		opts.Set("serial", serial)
582	}
583}
584
585// GetTokensResult models responses containing a list of tokens.
586type GetTokensResult struct {
587	duoapi.StatResult
588	ListResult
589	Response []Token
590}
591
592func (result *GetTokensResult) getResponse() interface{} {
593	return result.Response
594}
595
596func (result *GetTokensResult) appendResponse(tokens interface{}) {
597	asserted_tokens := tokens.([]Token)
598	result.Response = append(result.Response, asserted_tokens...)
599}
600
601// GetTokens calls GET /admin/v1/tokens
602// See https://duo.com/docs/adminapi#retrieve-hardware-tokens
603func (c *Client) GetTokens(options ...func(*url.Values)) (*GetTokensResult, error) {
604	params := url.Values{}
605	for _, o := range options {
606		o(&params)
607	}
608
609	cb := func(params url.Values) (responsePage, error) {
610		return c.retrieveTokens(params)
611	}
612	response, err := c.retrieveItems(params, cb)
613	if err != nil {
614		return nil, err
615	}
616
617	return response.(*GetTokensResult), nil
618}
619
620func (c *Client) retrieveTokens(params url.Values) (*GetTokensResult, error) {
621	_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/tokens", params, duoapi.UseTimeout)
622	if err != nil {
623		return nil, err
624	}
625
626	result := &GetTokensResult{}
627	err = json.Unmarshal(body, result)
628	if err != nil {
629		return nil, err
630	}
631	return result, nil
632}
633
634// GetTokenResult models responses containing a single token.
635type GetTokenResult struct {
636	duoapi.StatResult
637	Response Token
638}
639
640// GetToken calls GET /admin/v1/tokens/:token_id
641// See https://duo.com/docs/adminapi#retrieve-hardware-tokens
642func (c *Client) GetToken(tokenID string) (*GetTokenResult, error) {
643	path := fmt.Sprintf("/admin/v1/tokens/%s", tokenID)
644
645	_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
646	if err != nil {
647		return nil, err
648	}
649
650	result := &GetTokenResult{}
651	err = json.Unmarshal(body, result)
652	if err != nil {
653		return nil, err
654	}
655	return result, nil
656}
657
658// U2F token methods
659
660// GetU2FTokensResult models responses containing a list of U2F tokens.
661type GetU2FTokensResult struct {
662	duoapi.StatResult
663	ListResult
664	Response []U2FToken
665}
666
667func (result *GetU2FTokensResult) getResponse() interface{} {
668	return result.Response
669}
670
671func (result *GetU2FTokensResult) appendResponse(tokens interface{}) {
672	asserted_tokens := tokens.([]U2FToken)
673	result.Response = append(result.Response, asserted_tokens...)
674}
675
676// GetU2FTokens calls GET /admin/v1/u2ftokens
677// See https://duo.com/docs/adminapi#retrieve-u2f-tokens
678func (c *Client) GetU2FTokens(options ...func(*url.Values)) (*GetU2FTokensResult, error) {
679	params := url.Values{}
680	for _, o := range options {
681		o(&params)
682	}
683
684	cb := func(params url.Values) (responsePage, error) {
685		return c.retrieveU2FTokens(params)
686	}
687	response, err := c.retrieveItems(params, cb)
688	if err != nil {
689		return nil, err
690	}
691
692	return response.(*GetU2FTokensResult), nil
693}
694
695func (c *Client) retrieveU2FTokens(params url.Values) (*GetU2FTokensResult, error) {
696	_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/u2ftokens", params, duoapi.UseTimeout)
697	if err != nil {
698		return nil, err
699	}
700
701	result := &GetU2FTokensResult{}
702	err = json.Unmarshal(body, result)
703	if err != nil {
704		return nil, err
705	}
706	return result, nil
707}
708
709// GetU2FToken calls GET /admin/v1/u2ftokens/:registration_id
710// See https://duo.com/docs/adminapi#retrieve-u2f-token-by-id
711func (c *Client) GetU2FToken(registrationID string) (*GetU2FTokensResult, error) {
712	path := fmt.Sprintf("/admin/v1/u2ftokens/%s", registrationID)
713
714	_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
715	if err != nil {
716		return nil, err
717	}
718
719	result := &GetU2FTokensResult{}
720	err = json.Unmarshal(body, result)
721	if err != nil {
722		return nil, err
723	}
724	return result, nil
725}
726