package okta import ( "errors" "fmt" "net/url" "time" ) const ( profileEmailFilter = "profile.email" profileLoginFilter = "profile.login" profileStatusFilter = "status" profileIDFilter = "id" profileFirstNameFilter = "profile.firstName" profileLastNameFilter = "profile.lastName" profileLastUpdatedFilter = "lastUpdated" // UserStatusActive is a constant to represent OKTA User State returned by the API UserStatusActive = "ACTIVE" // UserStatusStaged is a constant to represent OKTA User State returned by the API UserStatusStaged = "STAGED" // UserStatusProvisioned is a constant to represent OKTA User State returned by the API UserStatusProvisioned = "PROVISIONED" // UserStatusRecovery is a constant to represent OKTA User State returned by the API UserStatusRecovery = "RECOVERY" // UserStatusLockedOut is a constant to represent OKTA User State returned by the API UserStatusLockedOut = "LOCKED_OUT" // UserStatusPasswordExpired is a constant to represent OKTA User State returned by the API UserStatusPasswordExpired = "PASSWORD_EXPIRED" // UserStatusSuspended is a constant to represent OKTA User State returned by the API UserStatusSuspended = "SUSPENDED" // UserStatusDeprovisioned is a constant to represent OKTA User State returned by the API UserStatusDeprovisioned = "DEPROVISIONED" oktaFilterTimeFormat = "2006-01-02T15:05:05.000Z" ) // UsersService handles communication with the User data related // methods of the OKTA API. type UsersService service // ActivationResponse - Response coming back from a user activation type ActivationResponse struct { ActivationURL string `json:"activationUrl"` } type provider struct { Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` } type recoveryQuestion struct { Question string `json:"question,omitempty"` Answer string `json:"answer,omitempty"` } type passwordValue struct { Value string `json:"value,omitempty"` } type credentials struct { Password *passwordValue `json:"password,omitempty"` Provider *provider `json:"provider,omitempty"` RecoveryQuestion *recoveryQuestion `json:"recovery_question,omitempty"` } type userProfile struct { Email string `json:"email"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Login string `json:"login"` MobilePhone string `json:"mobilePhone,omitempty"` SecondEmail string `json:"secondEmail,omitempty"` PsEmplid string `json:"psEmplid,omitempty"` NickName string `json:"nickname,omitempty"` DisplayName string `json:"displayName,omitempty"` ProfileURL string `json:"profileUrl,omitempty"` PreferredLanguage string `json:"preferredLanguage,omitempty"` UserType string `json:"userType,omitempty"` Organization string `json:"organization,omitempty"` Title string `json:"title,omitempty"` Division string `json:"division,omitempty"` Department string `json:"department,omitempty"` CostCenter string `json:"costCenter,omitempty"` EmployeeNumber string `json:"employeeNumber,omitempty"` PrimaryPhone string `json:"primaryPhone,omitempty"` StreetAddress string `json:"streetAddress,omitempty"` City string `json:"city,omitempty"` State string `json:"state,omitempty"` ZipCode string `json:"zipCode,omitempty"` CountryCode string `json:"countryCode,omitempty"` } type userLinks struct { ChangePassword struct { Href string `json:"href"` } `json:"changePassword"` ChangeRecoveryQuestion struct { Href string `json:"href"` } `json:"changeRecoveryQuestion"` Deactivate struct { Href string `json:"href"` } `json:"deactivate"` ExpirePassword struct { Href string `json:"href"` } `json:"expirePassword"` ForgotPassword struct { Href string `json:"href"` } `json:"forgotPassword"` ResetFactors struct { Href string `json:"href"` } `json:"resetFactors"` ResetPassword struct { Href string `json:"href"` } `json:"resetPassword"` } // User is a struct that represents a user object from OKTA. type User struct { Activated string `json:"activated,omitempty"` Created string `json:"created,omitempty"` Credentials credentials `json:"credentials,omitempty"` ID string `json:"id,omitempty"` LastLogin string `json:"lastLogin,omitempty"` LastUpdated string `json:"lastUpdated,omitempty"` PasswordChanged string `json:"passwordChanged,omitempty"` Profile userProfile `json:"profile"` Status string `json:"status,omitempty"` StatusChanged string `json:"statusChanged,omitempty"` Links userLinks `json:"_links,omitempty"` MFAFactors []userMFAFactor `json:"-,"` Groups []Group `json:"-"` } type userMFAFactor struct { ID string `json:"id,omitempty"` FactorType string `json:"factorType,omitempty"` Provider string `json:"provider,omitempty"` VendorName string `json:"vendorName,omitempty"` Status string `json:"status,omitempty"` Created time.Time `json:"created,omitempty"` LastUpdated time.Time `json:"lastUpdated,omitempty"` Profile struct { CredentialID string `json:"credentialId,omitempty"` } `json:"profile,omitempty"` } // NewUser object to create user objects in OKTA type NewUser struct { Profile userProfile `json:"profile"` Credentials *credentials `json:"credentials,omitempty"` } type newPasswordSet struct { Credentials credentials `json:"credentials"` } // ResetPasswordResponse struct that returns data about the password reset type ResetPasswordResponse struct { ResetPasswordURL string `json:"resetPasswordUrl"` } // NewUser - Returns a new user object. This is used to create users in OKTA. It only has the properties that // OKTA will take as input. The "User" object has more feilds that are OKTA returned like the ID, etc func (s *UsersService) NewUser() NewUser { return NewUser{} } // SetPassword Adds a specified password to the new User func (u *NewUser) SetPassword(passwordIn string) { if passwordIn != "" { pass := new(passwordValue) pass.Value = passwordIn var cred *credentials if u.Credentials == nil { cred = new(credentials) } else { cred = u.Credentials } cred.Password = pass u.Credentials = cred } } // SetRecoveryQuestion - Sets a custom security question and answer on a user object func (u *NewUser) SetRecoveryQuestion(questionIn string, answerIn string) { if questionIn != "" && answerIn != "" { recovery := new(recoveryQuestion) recovery.Question = questionIn recovery.Answer = answerIn var cred *credentials if u.Credentials == nil { cred = new(credentials) } else { cred = u.Credentials } cred.RecoveryQuestion = recovery u.Credentials = cred } } func (u User) String() string { return stringify(u) // return fmt.Sprintf("ID: %v \tLogin: %v", u.ID, u.Profile.Login) } // GetByID returns a user object for a specific OKTA ID. // 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 func (s *UsersService) GetByID(id string) (*User, *Response, error) { u := fmt.Sprintf("users/%v", id) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err } user := new(User) resp, err := s.client.Do(req, user) if err != nil { return nil, resp, err } return user, resp, err } // UserListFilterOptions is a struct that you can populate which will "filter" user searches // the exported struct fields should allow you to do different filters based on what is allowed in the OKTA API. // The filter OKTA API is limited in the fields it can search // NOTE: In the current form you can't add parenthesis and ordering // OKTA API Supports only a limited number of properties: // status, lastUpdated, id, profile.login, profile.email, profile.firstName, and profile.lastName. // http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter type UserListFilterOptions struct { Limit int `url:"limit,omitempty"` EmailEqualTo string `url:"-"` LoginEqualTo string `url:"-"` StatusEqualTo string `url:"-"` IDEqualTo string `url:"-"` FirstNameEqualTo string `url:"-"` LastNameEqualTo string `url:"-"` // API documenation says you can search with "starts with" but these don't work // FirstNameStartsWith string `url:"-"` // LastNameStartsWith string `url:"-"` // This will be built by internal - may not need to export FilterString string `url:"filter,omitempty"` NextURL *url.URL `url:"-"` GetAllPages bool `url:"-"` NumberOfPages int `url:"-"` LastUpdated dateFilter `url:"-"` } // PopulateGroups will populate the groups a user is a member of. You pass in a pointer to an existing users func (s *UsersService) PopulateGroups(user *User) (*Response, error) { u := fmt.Sprintf("users/%v/groups", user.ID) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } // Get first page of users. resp, err := s.client.Do(req, &user.Groups) if err != nil { return resp, err } // Look for any remaining user group pages. var nextURL string if resp.NextURL != nil { nextURL = resp.NextURL.String() } for { if nextURL != "" { req, err := s.client.NewRequest("GET", nextURL, nil) userGroupsPages := []Group{} resp, err := s.client.Do(req, &userGroupsPages) nextURL = "" if err != nil { return resp, err } user.Groups = append(user.Groups, userGroupsPages...) if resp.NextURL != nil { nextURL = resp.NextURL.String() } } else { return resp, err } } } // PopulateEnrolledFactors will populate the Enrolled MFA Factors a user is a member of. // You pass in a pointer to an existing users // http://developer.okta.com/docs/api/resources/factors.html#list-enrolled-factors func (s *UsersService) PopulateEnrolledFactors(user *User) (*Response, error) { u := fmt.Sprintf("users/%v/factors", user.ID) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } // TODO: If user has more than 200 groups this will only return those first 200 resp, err := s.client.Do(req, &user.MFAFactors) if err != nil { return resp, err } return resp, err } // List users with status of LOCKED_OUT // filter=status eq "LOCKED_OUT" // List users updated after 06/01/2013 but before 01/01/2014 // filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z" // List users updated after 06/01/2013 but before 01/01/2014 with a status of ACTIVE // filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z" and status eq "ACTIVE" // TODO - Currently no way to do parenthesis // List users updated after 06/01/2013 but with a status of LOCKED_OUT or RECOVERY // filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and (status eq "LOCKED_OUT" or status eq "RECOVERY") // OTKA API docs: http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter func appendToFilterString(currFilterString string, appendFilterKey string, appendFilterOperator string, appendFilterValue string) (rs string) { if currFilterString != "" { rs = fmt.Sprintf("%v and %v %v \"%v\"", currFilterString, appendFilterKey, appendFilterOperator, appendFilterValue) } else { rs = fmt.Sprintf("%v %v \"%v\"", appendFilterKey, appendFilterOperator, appendFilterValue) } return rs } // ListWithFilter will use the input UserListFilterOptions to find users and return a paged result set func (s *UsersService) ListWithFilter(opt *UserListFilterOptions) ([]User, *Response, error) { var u string var err error pagesRetreived := 0 if opt.NextURL != nil { u = opt.NextURL.String() } else { if opt.EmailEqualTo != "" { opt.FilterString = appendToFilterString(opt.FilterString, profileEmailFilter, FilterEqualOperator, opt.EmailEqualTo) } if opt.LoginEqualTo != "" { opt.FilterString = appendToFilterString(opt.FilterString, profileLoginFilter, FilterEqualOperator, opt.LoginEqualTo) } if opt.StatusEqualTo != "" { opt.FilterString = appendToFilterString(opt.FilterString, profileStatusFilter, FilterEqualOperator, opt.StatusEqualTo) } if opt.IDEqualTo != "" { opt.FilterString = appendToFilterString(opt.FilterString, profileIDFilter, FilterEqualOperator, opt.IDEqualTo) } if opt.FirstNameEqualTo != "" { opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, FilterEqualOperator, opt.FirstNameEqualTo) } if opt.LastNameEqualTo != "" { opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, FilterEqualOperator, opt.LastNameEqualTo) } // API documenation says you can search with "starts with" but these don't work // if opt.FirstNameStartsWith != "" { // opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, filterStartsWithOperator, opt.FirstNameStartsWith) // } // if opt.LastNameStartsWith != "" { // opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, filterStartsWithOperator, opt.LastNameStartsWith) // } if !opt.LastUpdated.Value.IsZero() { opt.FilterString = appendToFilterString(opt.FilterString, profileLastUpdatedFilter, opt.LastUpdated.Operator, opt.LastUpdated.Value.UTC().Format(oktaFilterTimeFormat)) } if opt.Limit == 0 { opt.Limit = defaultLimit } u, err = addOptions("users", opt) } if err != nil { return nil, nil, err } req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err } users := make([]User, 1) resp, err := s.client.Do(req, &users) if err != nil { return nil, resp, err } pagesRetreived++ if (opt.NumberOfPages > 0 && pagesRetreived < opt.NumberOfPages) || opt.GetAllPages { for { if pagesRetreived == opt.NumberOfPages { break } if resp.NextURL != nil { var userPage []User pageOption := new(UserListFilterOptions) pageOption.NextURL = resp.NextURL pageOption.NumberOfPages = 1 pageOption.Limit = opt.Limit userPage, resp, err = s.ListWithFilter(pageOption) if err != nil { return users, resp, err } users = append(users, userPage...) pagesRetreived++ } else { break } } } return users, resp, err } // Create - Creates a new user. You must pass in a "newUser" object created from Users.NewUser() // There are many differnt reasons that OKTA may reject the request so you have to check the error messages func (s *UsersService) Create(userIn NewUser, createAsActive bool) (*User, *Response, error) { u := fmt.Sprintf("users?activate=%v", createAsActive) req, err := s.client.NewRequest("POST", u, userIn) if err != nil { return nil, nil, err } newUser := new(User) resp, err := s.client.Do(req, newUser) if err != nil { return nil, resp, err } return newUser, resp, err } // Activate Activates a user. You can have OKTA send an email by including a "sendEmail=true" // If you pass in sendEmail=false, then activationResponse.ActivationURL will have a string URL that // can be sent to the end user. You can discard response if sendEmail=true func (s *UsersService) Activate(id string, sendEmail bool) (*ActivationResponse, *Response, error) { u := fmt.Sprintf("users/%v/lifecycle/activate?sendEmail=%v", id, sendEmail) req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, nil, err } activationInfo := new(ActivationResponse) resp, err := s.client.Do(req, activationInfo) if err != nil { return nil, resp, err } return activationInfo, resp, err } // Deactivate - Deactivates a user func (s *UsersService) Deactivate(id string) (*Response, error) { u := fmt.Sprintf("users/%v/lifecycle/deactivate", id) req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } resp, err := s.client.Do(req, nil) if err != nil { return resp, err } return resp, err } // Suspend - Suspends a user - If user is NOT active an Error will come back based on OKTA API: // http://developer.okta.com/docs/api/resources/users.html#suspend-user func (s *UsersService) Suspend(id string) (*Response, error) { u := fmt.Sprintf("users/%v/lifecycle/suspend", id) req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } resp, err := s.client.Do(req, nil) if err != nil { return resp, err } return resp, err } // Unsuspend - Unsuspends a user - If user is NOT SUSPENDED, an Error will come back based on OKTA API: // http://developer.okta.com/docs/api/resources/users.html#unsuspend-user func (s *UsersService) Unsuspend(id string) (*Response, error) { u := fmt.Sprintf("users/%v/lifecycle/unsuspend", id) req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } resp, err := s.client.Do(req, nil) if err != nil { return resp, err } return resp, err } // Unlock - Unlocks a user - Per docs, only for OKTA Mastered Account // http://developer.okta.com/docs/api/resources/users.html#unlock-user func (s *UsersService) Unlock(id string) (*Response, error) { u := fmt.Sprintf("users/%v/lifecycle/unlock", id) req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } resp, err := s.client.Do(req, nil) if err != nil { return resp, err } return resp, err } // SetPassword - Sets a user password to an Admin provided String func (s *UsersService) SetPassword(id string, newPassword string) (*User, *Response, error) { if id == "" || newPassword == "" { return nil, nil, errors.New("please provide a User ID and Password") } passwordUpdate := new(newPasswordSet) pass := new(passwordValue) pass.Value = newPassword passwordUpdate.Credentials.Password = pass u := fmt.Sprintf("users/%v", id) req, err := s.client.NewRequest("POST", u, passwordUpdate) if err != nil { return nil, nil, err } user := new(User) resp, err := s.client.Do(req, user) if err != nil { return nil, resp, err } return user, resp, err } // ResetPassword - Generates a one-time token (OTT) that can be used to reset a user’s password. // The OTT link can be automatically emailed to the user or returned to the API caller and distributed using a custom flow. // http://developer.okta.com/docs/api/resources/users.html#reset-password // If you pass in sendEmail=false, then resetPasswordResponse.resetPasswordUrl will have a string URL that // can be sent to the end user. You can discard response if sendEmail=true func (s *UsersService) ResetPassword(id string, sendEmail bool) (*ResetPasswordResponse, *Response, error) { u := fmt.Sprintf("users/%v/lifecycle/reset_password?sendEmail=%v", id, sendEmail) req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, nil, err } resetInfo := new(ResetPasswordResponse) resp, err := s.client.Do(req, resetInfo) if err != nil { return nil, resp, err } return resetInfo, resp, err } // PopulateMFAFactors will populate the MFA Factors a user is a member of. You pass in a pointer to an existing users func (s *UsersService) PopulateMFAFactors(user *User) (*Response, error) { u := fmt.Sprintf("users/%v/factors", user.ID) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } resp, err := s.client.Do(req, &user.MFAFactors) if err != nil { return resp, err } return resp, err }