1package okta 2 3import ( 4 "errors" 5 "fmt" 6 "net/url" 7 "time" 8) 9 10const ( 11 profileEmailFilter = "profile.email" 12 profileLoginFilter = "profile.login" 13 profileStatusFilter = "status" 14 profileIDFilter = "id" 15 profileFirstNameFilter = "profile.firstName" 16 profileLastNameFilter = "profile.lastName" 17 profileLastUpdatedFilter = "lastUpdated" 18 // UserStatusActive is a constant to represent OKTA User State returned by the API 19 UserStatusActive = "ACTIVE" 20 // UserStatusStaged is a constant to represent OKTA User State returned by the API 21 UserStatusStaged = "STAGED" 22 // UserStatusProvisioned is a constant to represent OKTA User State returned by the API 23 UserStatusProvisioned = "PROVISIONED" 24 // UserStatusRecovery is a constant to represent OKTA User State returned by the API 25 UserStatusRecovery = "RECOVERY" 26 // UserStatusLockedOut is a constant to represent OKTA User State returned by the API 27 UserStatusLockedOut = "LOCKED_OUT" 28 // UserStatusPasswordExpired is a constant to represent OKTA User State returned by the API 29 UserStatusPasswordExpired = "PASSWORD_EXPIRED" 30 // UserStatusSuspended is a constant to represent OKTA User State returned by the API 31 UserStatusSuspended = "SUSPENDED" 32 // UserStatusDeprovisioned is a constant to represent OKTA User State returned by the API 33 UserStatusDeprovisioned = "DEPROVISIONED" 34 35 oktaFilterTimeFormat = "2006-01-02T15:05:05.000Z" 36) 37 38// UsersService handles communication with the User data related 39// methods of the OKTA API. 40type UsersService service 41 42// ActivationResponse - Response coming back from a user activation 43type ActivationResponse struct { 44 ActivationURL string `json:"activationUrl"` 45} 46 47type provider struct { 48 Name string `json:"name,omitempty"` 49 Type string `json:"type,omitempty"` 50} 51 52type recoveryQuestion struct { 53 Question string `json:"question,omitempty"` 54 Answer string `json:"answer,omitempty"` 55} 56 57type passwordValue struct { 58 Value string `json:"value,omitempty"` 59} 60type credentials struct { 61 Password *passwordValue `json:"password,omitempty"` 62 Provider *provider `json:"provider,omitempty"` 63 RecoveryQuestion *recoveryQuestion `json:"recovery_question,omitempty"` 64} 65 66type userProfile struct { 67 Email string `json:"email"` 68 FirstName string `json:"firstName"` 69 LastName string `json:"lastName"` 70 Login string `json:"login"` 71 MobilePhone string `json:"mobilePhone,omitempty"` 72 SecondEmail string `json:"secondEmail,omitempty"` 73 PsEmplid string `json:"psEmplid,omitempty"` 74 NickName string `json:"nickname,omitempty"` 75 DisplayName string `json:"displayName,omitempty"` 76 77 ProfileURL string `json:"profileUrl,omitempty"` 78 PreferredLanguage string `json:"preferredLanguage,omitempty"` 79 UserType string `json:"userType,omitempty"` 80 Organization string `json:"organization,omitempty"` 81 Title string `json:"title,omitempty"` 82 Division string `json:"division,omitempty"` 83 Department string `json:"department,omitempty"` 84 CostCenter string `json:"costCenter,omitempty"` 85 EmployeeNumber string `json:"employeeNumber,omitempty"` 86 PrimaryPhone string `json:"primaryPhone,omitempty"` 87 StreetAddress string `json:"streetAddress,omitempty"` 88 City string `json:"city,omitempty"` 89 State string `json:"state,omitempty"` 90 ZipCode string `json:"zipCode,omitempty"` 91 CountryCode string `json:"countryCode,omitempty"` 92} 93 94type userLinks struct { 95 ChangePassword struct { 96 Href string `json:"href"` 97 } `json:"changePassword"` 98 ChangeRecoveryQuestion struct { 99 Href string `json:"href"` 100 } `json:"changeRecoveryQuestion"` 101 Deactivate struct { 102 Href string `json:"href"` 103 } `json:"deactivate"` 104 ExpirePassword struct { 105 Href string `json:"href"` 106 } `json:"expirePassword"` 107 ForgotPassword struct { 108 Href string `json:"href"` 109 } `json:"forgotPassword"` 110 ResetFactors struct { 111 Href string `json:"href"` 112 } `json:"resetFactors"` 113 ResetPassword struct { 114 Href string `json:"href"` 115 } `json:"resetPassword"` 116} 117 118// User is a struct that represents a user object from OKTA. 119type User struct { 120 Activated string `json:"activated,omitempty"` 121 Created string `json:"created,omitempty"` 122 Credentials credentials `json:"credentials,omitempty"` 123 ID string `json:"id,omitempty"` 124 LastLogin string `json:"lastLogin,omitempty"` 125 LastUpdated string `json:"lastUpdated,omitempty"` 126 PasswordChanged string `json:"passwordChanged,omitempty"` 127 Profile userProfile `json:"profile"` 128 Status string `json:"status,omitempty"` 129 StatusChanged string `json:"statusChanged,omitempty"` 130 Links userLinks `json:"_links,omitempty"` 131 MFAFactors []userMFAFactor `json:"-,"` 132 Groups []Group `json:"-"` 133} 134 135type userMFAFactor struct { 136 ID string `json:"id,omitempty"` 137 FactorType string `json:"factorType,omitempty"` 138 Provider string `json:"provider,omitempty"` 139 VendorName string `json:"vendorName,omitempty"` 140 Status string `json:"status,omitempty"` 141 Created time.Time `json:"created,omitempty"` 142 LastUpdated time.Time `json:"lastUpdated,omitempty"` 143 Profile struct { 144 CredentialID string `json:"credentialId,omitempty"` 145 } `json:"profile,omitempty"` 146} 147 148// NewUser object to create user objects in OKTA 149type NewUser struct { 150 Profile userProfile `json:"profile"` 151 Credentials *credentials `json:"credentials,omitempty"` 152} 153 154type newPasswordSet struct { 155 Credentials credentials `json:"credentials"` 156} 157 158// ResetPasswordResponse struct that returns data about the password reset 159type ResetPasswordResponse struct { 160 ResetPasswordURL string `json:"resetPasswordUrl"` 161} 162 163// NewUser - Returns a new user object. This is used to create users in OKTA. It only has the properties that 164// OKTA will take as input. The "User" object has more feilds that are OKTA returned like the ID, etc 165func (s *UsersService) NewUser() NewUser { 166 return NewUser{} 167} 168 169// SetPassword Adds a specified password to the new User 170func (u *NewUser) SetPassword(passwordIn string) { 171 172 if passwordIn != "" { 173 174 pass := new(passwordValue) 175 pass.Value = passwordIn 176 177 var cred *credentials 178 if u.Credentials == nil { 179 cred = new(credentials) 180 } else { 181 cred = u.Credentials 182 } 183 184 cred.Password = pass 185 u.Credentials = cred 186 187 } 188} 189 190// SetRecoveryQuestion - Sets a custom security question and answer on a user object 191func (u *NewUser) SetRecoveryQuestion(questionIn string, answerIn string) { 192 193 if questionIn != "" && answerIn != "" { 194 recovery := new(recoveryQuestion) 195 196 recovery.Question = questionIn 197 recovery.Answer = answerIn 198 199 var cred *credentials 200 if u.Credentials == nil { 201 cred = new(credentials) 202 } else { 203 cred = u.Credentials 204 } 205 cred.RecoveryQuestion = recovery 206 u.Credentials = cred 207 208 } 209} 210 211func (u User) String() string { 212 return stringify(u) 213 // return fmt.Sprintf("ID: %v \tLogin: %v", u.ID, u.Profile.Login) 214} 215 216// GetByID returns a user object for a specific OKTA ID. 217// Generally the id input string is the cryptic OKTA key value from User.ID. However, the OKTA API may accept other values like "me", or login shortname 218func (s *UsersService) GetByID(id string) (*User, *Response, error) { 219 u := fmt.Sprintf("users/%v", id) 220 req, err := s.client.NewRequest("GET", u, nil) 221 if err != nil { 222 return nil, nil, err 223 } 224 225 user := new(User) 226 resp, err := s.client.Do(req, user) 227 if err != nil { 228 return nil, resp, err 229 } 230 231 return user, resp, err 232} 233 234// UserListFilterOptions is a struct that you can populate which will "filter" user searches 235// the exported struct fields should allow you to do different filters based on what is allowed in the OKTA API. 236// The filter OKTA API is limited in the fields it can search 237// NOTE: In the current form you can't add parenthesis and ordering 238// OKTA API Supports only a limited number of properties: 239// status, lastUpdated, id, profile.login, profile.email, profile.firstName, and profile.lastName. 240// http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter 241type UserListFilterOptions struct { 242 Limit int `url:"limit,omitempty"` 243 EmailEqualTo string `url:"-"` 244 LoginEqualTo string `url:"-"` 245 StatusEqualTo string `url:"-"` 246 IDEqualTo string `url:"-"` 247 248 FirstNameEqualTo string `url:"-"` 249 LastNameEqualTo string `url:"-"` 250 // API documenation says you can search with "starts with" but these don't work 251 252 // FirstNameStartsWith string `url:"-"` 253 // LastNameStartsWith string `url:"-"` 254 255 // This will be built by internal - may not need to export 256 FilterString string `url:"filter,omitempty"` 257 NextURL *url.URL `url:"-"` 258 GetAllPages bool `url:"-"` 259 NumberOfPages int `url:"-"` 260 LastUpdated dateFilter `url:"-"` 261} 262 263// PopulateGroups will populate the groups a user is a member of. You pass in a pointer to an existing users 264func (s *UsersService) PopulateGroups(user *User) (*Response, error) { 265 u := fmt.Sprintf("users/%v/groups", user.ID) 266 req, err := s.client.NewRequest("GET", u, nil) 267 268 if err != nil { 269 return nil, err 270 } 271 // Get first page of users. 272 resp, err := s.client.Do(req, &user.Groups) 273 if err != nil { 274 return resp, err 275 } 276 // Look for any remaining user group pages. 277 var nextURL string 278 if resp.NextURL != nil { 279 nextURL = resp.NextURL.String() 280 } 281 for { 282 283 if nextURL != "" { 284 req, err := s.client.NewRequest("GET", nextURL, nil) 285 userGroupsPages := []Group{} 286 287 resp, err := s.client.Do(req, &userGroupsPages) 288 nextURL = "" 289 if err != nil { 290 return resp, err 291 } 292 user.Groups = append(user.Groups, userGroupsPages...) 293 if resp.NextURL != nil { 294 nextURL = resp.NextURL.String() 295 } 296 297 } else { 298 return resp, err 299 } 300 301 } 302 303} 304 305// PopulateEnrolledFactors will populate the Enrolled MFA Factors a user is a member of. 306// You pass in a pointer to an existing users 307// http://developer.okta.com/docs/api/resources/factors.html#list-enrolled-factors 308func (s *UsersService) PopulateEnrolledFactors(user *User) (*Response, error) { 309 u := fmt.Sprintf("users/%v/factors", user.ID) 310 req, err := s.client.NewRequest("GET", u, nil) 311 312 if err != nil { 313 return nil, err 314 } 315 // TODO: If user has more than 200 groups this will only return those first 200 316 resp, err := s.client.Do(req, &user.MFAFactors) 317 if err != nil { 318 return resp, err 319 } 320 321 return resp, err 322} 323 324// List users with status of LOCKED_OUT 325// filter=status eq "LOCKED_OUT" 326// List users updated after 06/01/2013 but before 01/01/2014 327// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z" 328// List users updated after 06/01/2013 but before 01/01/2014 with a status of ACTIVE 329// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z" and status eq "ACTIVE" 330// TODO - Currently no way to do parenthesis 331// List users updated after 06/01/2013 but with a status of LOCKED_OUT or RECOVERY 332// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and (status eq "LOCKED_OUT" or status eq "RECOVERY") 333 334// OTKA API docs: http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter 335 336func appendToFilterString(currFilterString string, appendFilterKey string, appendFilterOperator string, appendFilterValue string) (rs string) { 337 if currFilterString != "" { 338 rs = fmt.Sprintf("%v and %v %v \"%v\"", currFilterString, appendFilterKey, appendFilterOperator, appendFilterValue) 339 } else { 340 rs = fmt.Sprintf("%v %v \"%v\"", appendFilterKey, appendFilterOperator, appendFilterValue) 341 } 342 343 return rs 344} 345 346// ListWithFilter will use the input UserListFilterOptions to find users and return a paged result set 347func (s *UsersService) ListWithFilter(opt *UserListFilterOptions) ([]User, *Response, error) { 348 var u string 349 var err error 350 351 pagesRetreived := 0 352 353 if opt.NextURL != nil { 354 u = opt.NextURL.String() 355 } else { 356 if opt.EmailEqualTo != "" { 357 opt.FilterString = appendToFilterString(opt.FilterString, profileEmailFilter, FilterEqualOperator, opt.EmailEqualTo) 358 } 359 if opt.LoginEqualTo != "" { 360 opt.FilterString = appendToFilterString(opt.FilterString, profileLoginFilter, FilterEqualOperator, opt.LoginEqualTo) 361 } 362 363 if opt.StatusEqualTo != "" { 364 opt.FilterString = appendToFilterString(opt.FilterString, profileStatusFilter, FilterEqualOperator, opt.StatusEqualTo) 365 } 366 367 if opt.IDEqualTo != "" { 368 opt.FilterString = appendToFilterString(opt.FilterString, profileIDFilter, FilterEqualOperator, opt.IDEqualTo) 369 } 370 371 if opt.FirstNameEqualTo != "" { 372 opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, FilterEqualOperator, opt.FirstNameEqualTo) 373 } 374 375 if opt.LastNameEqualTo != "" { 376 opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, FilterEqualOperator, opt.LastNameEqualTo) 377 } 378 379 // API documenation says you can search with "starts with" but these don't work 380 // if opt.FirstNameStartsWith != "" { 381 // opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, filterStartsWithOperator, opt.FirstNameStartsWith) 382 // } 383 384 // if opt.LastNameStartsWith != "" { 385 // opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, filterStartsWithOperator, opt.LastNameStartsWith) 386 // } 387 388 if !opt.LastUpdated.Value.IsZero() { 389 opt.FilterString = appendToFilterString(opt.FilterString, profileLastUpdatedFilter, opt.LastUpdated.Operator, opt.LastUpdated.Value.UTC().Format(oktaFilterTimeFormat)) 390 } 391 392 if opt.Limit == 0 { 393 opt.Limit = defaultLimit 394 } 395 396 u, err = addOptions("users", opt) 397 398 } 399 400 if err != nil { 401 return nil, nil, err 402 } 403 404 req, err := s.client.NewRequest("GET", u, nil) 405 if err != nil { 406 return nil, nil, err 407 } 408 users := make([]User, 1) 409 resp, err := s.client.Do(req, &users) 410 if err != nil { 411 return nil, resp, err 412 } 413 414 pagesRetreived++ 415 416 if (opt.NumberOfPages > 0 && pagesRetreived < opt.NumberOfPages) || opt.GetAllPages { 417 418 for { 419 420 if pagesRetreived == opt.NumberOfPages { 421 break 422 } 423 if resp.NextURL != nil { 424 var userPage []User 425 pageOption := new(UserListFilterOptions) 426 pageOption.NextURL = resp.NextURL 427 pageOption.NumberOfPages = 1 428 pageOption.Limit = opt.Limit 429 430 userPage, resp, err = s.ListWithFilter(pageOption) 431 if err != nil { 432 return users, resp, err 433 } 434 users = append(users, userPage...) 435 pagesRetreived++ 436 } else { 437 break 438 } 439 } 440 } 441 return users, resp, err 442} 443 444// Create - Creates a new user. You must pass in a "newUser" object created from Users.NewUser() 445// There are many differnt reasons that OKTA may reject the request so you have to check the error messages 446func (s *UsersService) Create(userIn NewUser, createAsActive bool) (*User, *Response, error) { 447 448 u := fmt.Sprintf("users?activate=%v", createAsActive) 449 450 req, err := s.client.NewRequest("POST", u, userIn) 451 452 if err != nil { 453 return nil, nil, err 454 } 455 456 newUser := new(User) 457 resp, err := s.client.Do(req, newUser) 458 if err != nil { 459 return nil, resp, err 460 } 461 462 return newUser, resp, err 463} 464 465// Activate Activates a user. You can have OKTA send an email by including a "sendEmail=true" 466// If you pass in sendEmail=false, then activationResponse.ActivationURL will have a string URL that 467// can be sent to the end user. You can discard response if sendEmail=true 468func (s *UsersService) Activate(id string, sendEmail bool) (*ActivationResponse, *Response, error) { 469 u := fmt.Sprintf("users/%v/lifecycle/activate?sendEmail=%v", id, sendEmail) 470 471 req, err := s.client.NewRequest("POST", u, nil) 472 if err != nil { 473 return nil, nil, err 474 } 475 476 activationInfo := new(ActivationResponse) 477 resp, err := s.client.Do(req, activationInfo) 478 479 if err != nil { 480 return nil, resp, err 481 } 482 483 return activationInfo, resp, err 484} 485 486// Deactivate - Deactivates a user 487func (s *UsersService) Deactivate(id string) (*Response, error) { 488 u := fmt.Sprintf("users/%v/lifecycle/deactivate", id) 489 490 req, err := s.client.NewRequest("POST", u, nil) 491 if err != nil { 492 return nil, err 493 } 494 resp, err := s.client.Do(req, nil) 495 496 if err != nil { 497 return resp, err 498 } 499 500 return resp, err 501} 502 503// Suspend - Suspends a user - If user is NOT active an Error will come back based on OKTA API: 504// http://developer.okta.com/docs/api/resources/users.html#suspend-user 505func (s *UsersService) Suspend(id string) (*Response, error) { 506 u := fmt.Sprintf("users/%v/lifecycle/suspend", id) 507 508 req, err := s.client.NewRequest("POST", u, nil) 509 if err != nil { 510 return nil, err 511 } 512 resp, err := s.client.Do(req, nil) 513 514 if err != nil { 515 return resp, err 516 } 517 518 return resp, err 519} 520 521// Unsuspend - Unsuspends a user - If user is NOT SUSPENDED, an Error will come back based on OKTA API: 522// http://developer.okta.com/docs/api/resources/users.html#unsuspend-user 523func (s *UsersService) Unsuspend(id string) (*Response, error) { 524 u := fmt.Sprintf("users/%v/lifecycle/unsuspend", id) 525 526 req, err := s.client.NewRequest("POST", u, nil) 527 if err != nil { 528 return nil, err 529 } 530 resp, err := s.client.Do(req, nil) 531 532 if err != nil { 533 return resp, err 534 } 535 536 return resp, err 537} 538 539// Unlock - Unlocks a user - Per docs, only for OKTA Mastered Account 540// http://developer.okta.com/docs/api/resources/users.html#unlock-user 541func (s *UsersService) Unlock(id string) (*Response, error) { 542 u := fmt.Sprintf("users/%v/lifecycle/unlock", id) 543 544 req, err := s.client.NewRequest("POST", u, nil) 545 if err != nil { 546 return nil, err 547 } 548 resp, err := s.client.Do(req, nil) 549 550 if err != nil { 551 return resp, err 552 } 553 554 return resp, err 555} 556 557// SetPassword - Sets a user password to an Admin provided String 558func (s *UsersService) SetPassword(id string, newPassword string) (*User, *Response, error) { 559 560 if id == "" || newPassword == "" { 561 return nil, nil, errors.New("please provide a User ID and Password") 562 } 563 564 passwordUpdate := new(newPasswordSet) 565 566 pass := new(passwordValue) 567 pass.Value = newPassword 568 569 passwordUpdate.Credentials.Password = pass 570 571 u := fmt.Sprintf("users/%v", id) 572 req, err := s.client.NewRequest("POST", u, passwordUpdate) 573 if err != nil { 574 return nil, nil, err 575 } 576 577 user := new(User) 578 resp, err := s.client.Do(req, user) 579 if err != nil { 580 return nil, resp, err 581 } 582 583 return user, resp, err 584} 585 586// ResetPassword - Generates a one-time token (OTT) that can be used to reset a user’s password. 587// The OTT link can be automatically emailed to the user or returned to the API caller and distributed using a custom flow. 588// http://developer.okta.com/docs/api/resources/users.html#reset-password 589// If you pass in sendEmail=false, then resetPasswordResponse.resetPasswordUrl will have a string URL that 590// can be sent to the end user. You can discard response if sendEmail=true 591func (s *UsersService) ResetPassword(id string, sendEmail bool) (*ResetPasswordResponse, *Response, error) { 592 u := fmt.Sprintf("users/%v/lifecycle/reset_password?sendEmail=%v", id, sendEmail) 593 594 req, err := s.client.NewRequest("POST", u, nil) 595 if err != nil { 596 return nil, nil, err 597 } 598 599 resetInfo := new(ResetPasswordResponse) 600 resp, err := s.client.Do(req, resetInfo) 601 602 if err != nil { 603 return nil, resp, err 604 } 605 606 return resetInfo, resp, err 607} 608 609// PopulateMFAFactors will populate the MFA Factors a user is a member of. You pass in a pointer to an existing users 610func (s *UsersService) PopulateMFAFactors(user *User) (*Response, error) { 611 u := fmt.Sprintf("users/%v/factors", user.ID) 612 613 req, err := s.client.NewRequest("GET", u, nil) 614 615 if err != nil { 616 return nil, err 617 } 618 619 resp, err := s.client.Do(req, &user.MFAFactors) 620 if err != nil { 621 return resp, err 622 } 623 624 return resp, err 625} 626