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(¶ms) 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(¶ms) 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(¶ms) 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(¶ms) 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(¶ms) 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(¶ms) 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(¶ms) 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(¶ms) 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(¶ms) 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