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