2017-08-31 02:37:21 +00:00
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 {
2017-08-31 02:37:21 +00:00
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:"-" `
2017-08-31 02:37:21 +00:00
}
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 {
2017-08-31 02:37:21 +00:00
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.
2017-08-31 02:37:21 +00:00
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
}
}
2017-08-31 02:37:21 +00:00
}
// 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 ++
2017-08-31 02:37:21 +00:00
} 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 ) {
2017-08-31 02:37:21 +00:00
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 )
2017-08-31 02:37:21 +00:00
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
2018-02-14 14:38:46 +00:00
func ( s * UsersService ) ResetPassword ( id string , sendEmail bool ) ( * ResetPasswordResponse , * Response , error ) {
2017-08-31 02:37:21 +00:00
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 )
2017-08-31 02:37:21 +00:00
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
}