open-vault/vendor/github.com/chrismalek/oktasdk-go/okta/users.go

626 lines
19 KiB
Go
Raw Normal View History

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
2018-02-14 14:38:46 +00:00
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"`
2018-02-14 14:38:46 +00:00
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"`
}
2018-02-14 14:38:46 +00:00
// 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
}
2018-02-14 14:38:46 +00:00
// Get first page of users.
resp, err := s.client.Do(req, &user.Groups)
if err != nil {
return resp, err
}
2018-02-14 14:38:46 +00:00
// 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
}
2018-02-14 14:38:46 +00:00
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
2018-02-14 14:38:46 +00:00
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
}
2018-02-14 14:38:46 +00:00
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 users 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
2018-02-14 14:38:46 +00:00
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
}
2018-02-14 14:38:46 +00:00
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
}