From e54a3dbe476b756786294336460755485c08574a Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Wed, 30 Aug 2017 22:37:21 -0400 Subject: [PATCH] Updating Okta lib for credential backend (#3245) * migrating to chrismalek/oktasdk-go Okta library * updating path docs * updating bool reference from config --- builtin/credential/okta/backend.go | 50 +- builtin/credential/okta/path_config.go | 85 ++- .../chrismalek/oktasdk-go/LICENSE.txt | 21 + .../chrismalek/oktasdk-go/okta/apps.go | 242 +++++++ .../chrismalek/oktasdk-go/okta/factors.go | 8 + .../chrismalek/oktasdk-go/okta/groups.go | 306 +++++++++ .../chrismalek/oktasdk-go/okta/sdk.go | 503 +++++++++++++++ .../chrismalek/oktasdk-go/okta/users.go | 600 ++++++++++++++++++ vendor/github.com/sstarcher/go-okta/README.md | 9 - vendor/github.com/sstarcher/go-okta/api.go | 123 ---- vendor/github.com/sstarcher/go-okta/authn.go | 45 -- .../github.com/sstarcher/go-okta/circleci.yml | 19 - .../github.com/sstarcher/go-okta/glide.lock | 4 - .../github.com/sstarcher/go-okta/glide.yaml | 2 - .../github.com/sstarcher/go-okta/sessions.go | 46 -- vendor/github.com/sstarcher/go-okta/users.go | 83 --- vendor/vendor.json | 12 +- website/source/api/auth/okta/index.html.md | 21 +- 18 files changed, 1788 insertions(+), 391 deletions(-) create mode 100644 vendor/github.com/chrismalek/oktasdk-go/LICENSE.txt create mode 100644 vendor/github.com/chrismalek/oktasdk-go/okta/apps.go create mode 100644 vendor/github.com/chrismalek/oktasdk-go/okta/factors.go create mode 100644 vendor/github.com/chrismalek/oktasdk-go/okta/groups.go create mode 100644 vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go create mode 100644 vendor/github.com/chrismalek/oktasdk-go/okta/users.go delete mode 100644 vendor/github.com/sstarcher/go-okta/README.md delete mode 100644 vendor/github.com/sstarcher/go-okta/api.go delete mode 100644 vendor/github.com/sstarcher/go-okta/authn.go delete mode 100644 vendor/github.com/sstarcher/go-okta/circleci.yml delete mode 100644 vendor/github.com/sstarcher/go-okta/glide.lock delete mode 100644 vendor/github.com/sstarcher/go-okta/glide.yaml delete mode 100644 vendor/github.com/sstarcher/go-okta/sessions.go delete mode 100644 vendor/github.com/sstarcher/go-okta/users.go diff --git a/builtin/credential/okta/backend.go b/builtin/credential/okta/backend.go index 0977b2c23ff6..969fd4272194 100644 --- a/builtin/credential/okta/backend.go +++ b/builtin/credential/okta/backend.go @@ -3,6 +3,7 @@ package okta import ( "fmt" + "github.com/chrismalek/oktasdk-go/okta" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -56,18 +57,44 @@ func (b *backend) Login(req *logical.Request, username string, password string) } client := cfg.OktaClient() - auth, err := client.Authenticate(username, password) + + type embeddedResult struct { + User okta.User `json:"user"` + } + + type authResult struct { + Embedded embeddedResult `json:"_embedded"` + } + + authReq, err := client.NewRequest("POST", "authn", map[string]interface{}{ + "username": username, + "password": password, + }) + if err != nil { + return nil, nil, err + } + + var result authResult + rsp, err := client.Do(authReq, &result) if err != nil { return nil, logical.ErrorResponse(fmt.Sprintf("Okta auth failed: %v", err)), nil } - if auth == nil { + if rsp == nil { return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil } - oktaGroups, err := b.getOktaGroups(cfg, auth.Embedded.User.ID) + oktaUser := &result.Embedded.User + rsp, err = client.Users.PopulateGroups(oktaUser) if err != nil { return nil, logical.ErrorResponse(err.Error()), nil } + if rsp == nil { + return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil + } + oktaGroups := make([]string, 0, len(oktaUser.Groups)) + for _, group := range oktaUser.Groups { + oktaGroups = append(oktaGroups, group.Profile.Name) + } if b.Logger().IsDebug() { b.Logger().Debug("auth/okta: Groups fetched from Okta", "num_groups", len(oktaGroups), "groups", oktaGroups) } @@ -130,23 +157,6 @@ func (b *backend) Login(req *logical.Request, username string, password string) return policies, oktaResponse, nil } -func (b *backend) getOktaGroups(cfg *ConfigEntry, userID string) ([]string, error) { - if cfg.Token != "" { - client := cfg.OktaClient() - groups, err := client.Groups(userID) - if err != nil { - return nil, err - } - - oktaGroups := make([]string, 0, len(*groups)) - for _, group := range *groups { - oktaGroups = append(oktaGroups, group.Profile.Name) - } - return oktaGroups, err - } - return nil, nil -} - const backendHelp = ` The Okta credential provider allows authentication querying, checking username and password, and associating policies. If an api token is configure diff --git a/builtin/credential/okta/path_config.go b/builtin/credential/okta/path_config.go index b39d95265390..19014eddf7c1 100644 --- a/builtin/credential/okta/path_config.go +++ b/builtin/credential/okta/path_config.go @@ -3,12 +3,14 @@ package okta import ( "fmt" "net/url" + "strings" "time" + "github.com/chrismalek/oktasdk-go/okta" + "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - "github.com/sstarcher/go-okta" ) func pathConfig(b *backend) *framework.Path { @@ -17,16 +19,29 @@ func pathConfig(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "organization": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Okta organization to authenticate against", + Description: "Okta organization to authenticate against (DEPRECATED)", + }, + "org_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the organization to be used in the Okta API.", }, "token": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Okta admin API token", + Description: "Okta admin API token (DEPRECATED)", + }, + "api_token": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Okta API key.", }, "base_url": &framework.FieldSchema{ Type: framework.TypeString, Description: `The API endpoint to use. Useful if you -are using Okta development accounts.`, +are using Okta development accounts. (DEPRECATED)`, + }, + "production": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: true, + Description: `If set, production API URL prefix will be used to communicate with Okta and if not set, a preview production API URL prefix will be used. Defaults to true.`, }, "ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -84,11 +99,15 @@ func (b *backend) pathConfigRead( resp := &logical.Response{ Data: map[string]interface{}{ "organization": cfg.Org, - "base_url": cfg.BaseURL, + "org_name": cfg.Org, + "production": *cfg.Production, "ttl": cfg.TTL, "max_ttl": cfg.MaxTTL, }, } + if cfg.BaseURL != "" { + resp.Data["base_url"] = cfg.BaseURL + } return resp, nil } @@ -106,18 +125,32 @@ func (b *backend) pathConfigWrite( cfg = &ConfigEntry{} } - org, ok := d.GetOk("organization") + org, ok := d.GetOk("org_name") if ok { cfg.Org = org.(string) - } else if req.Operation == logical.CreateOperation { - cfg.Org = d.Get("organization").(string) + } + if cfg.Org == "" { + org, ok = d.GetOk("organization") + if ok { + cfg.Org = org.(string) + } + } + if cfg.Org == "" && req.Operation == logical.CreateOperation { + return logical.ErrorResponse("org_name is missing"), nil } - token, ok := d.GetOk("token") + token, ok := d.GetOk("api_token") if ok { cfg.Token = token.(string) - } else if req.Operation == logical.CreateOperation { - cfg.Token = d.Get("token").(string) + } + if cfg.Token == "" { + token, ok = d.GetOk("token") + if ok { + cfg.Token = token.(string) + } + } + if cfg.Token == "" && req.Operation == logical.CreateOperation { + return logical.ErrorResponse("api_token is missing"), nil } baseURL, ok := d.GetOk("base_url") @@ -134,6 +167,9 @@ func (b *backend) pathConfigWrite( cfg.BaseURL = d.Get("base_url").(string) } + productionRaw := d.Get("production").(bool) + cfg.Production = &productionRaw + ttl, ok := d.GetOk("ttl") if ok { cfg.TTL = time.Duration(ttl.(int)) * time.Second @@ -171,25 +207,26 @@ func (b *backend) pathConfigExistenceCheck( // OktaClient creates a basic okta client connection func (c *ConfigEntry) OktaClient() *okta.Client { - client := okta.NewClient(c.Org) - if c.BaseURL != "" { - client.Url = c.BaseURL + production := true + if c.Production != nil { + production = *c.Production } - - if c.Token != "" { - client.ApiToken = c.Token + if c.BaseURL != "" { + if strings.Contains(c.BaseURL, "oktapreview.com") { + production = false + } } - - return client + return okta.NewClient(cleanhttp.DefaultClient(), c.Org, c.Token, production) } // ConfigEntry for Okta type ConfigEntry struct { - Org string `json:"organization"` - Token string `json:"token"` - BaseURL string `json:"base_url"` - TTL time.Duration `json:"ttl"` - MaxTTL time.Duration `json:"max_ttl"` + Org string `json:"organization"` + Token string `json:"token"` + BaseURL string `json:"base_url"` + Production *bool `json:"is_production,omitempty"` + TTL time.Duration `json:"ttl"` + MaxTTL time.Duration `json:"max_ttl"` } const pathConfigHelp = ` diff --git a/vendor/github.com/chrismalek/oktasdk-go/LICENSE.txt b/vendor/github.com/chrismalek/oktasdk-go/LICENSE.txt new file mode 100644 index 000000000000..63b4b681cb65 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/apps.go b/vendor/github.com/chrismalek/oktasdk-go/okta/apps.go new file mode 100644 index 000000000000..d198606758d6 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/apps.go @@ -0,0 +1,242 @@ +package okta + +import ( + "fmt" + "net/url" + "time" +) + +type AppsService service + +// AppFilterOptions is used to generate a "Filter" to search for different Apps +// The values here coorelate to API Search paramgters on the group API +type AppFilterOptions struct { + NextURL *url.URL `url:"-"` + GetAllPages bool `url:"-"` + NumberOfPages int `url:"-"` + Limit int `url:"limit,omitempty"` +} + +type App struct { + ID string `json:"id"` + Name string `json:"name"` + Label string `json:"label"` + Status string `json:"status"` + LastUpdated time.Time `json:"lastUpdated"` + Created time.Time `json:"created"` + Accessibility struct { + SelfService bool `json:"selfService"` + ErrorRedirectURL interface{} `json:"errorRedirectUrl"` + LoginRedirectURL interface{} `json:"loginRedirectUrl"` + } `json:"accessibility"` + Visibility struct { + AutoSubmitToolbar bool `json:"autoSubmitToolbar"` + Hide struct { + IOS bool `json:"iOS"` + Web bool `json:"web"` + } `json:"hide"` + AppLinks struct { + TestorgoneCustomsaml20App1Link bool `json:"testorgone_customsaml20app_1_link"` + } `json:"appLinks"` + } `json:"visibility"` + Features []interface{} `json:"features"` + SignOnMode string `json:"signOnMode"` + Credentials struct { + UserNameTemplate struct { + Template string `json:"template"` + Type string `json:"type"` + } `json:"userNameTemplate"` + Signing struct { + } `json:"signing"` + } `json:"credentials"` + Settings struct { + App struct { + } `json:"app"` + Notifications struct { + Vpn struct { + Network struct { + Connection string `json:"connection"` + } `json:"network"` + Message interface{} `json:"message"` + HelpURL interface{} `json:"helpUrl"` + } `json:"vpn"` + } `json:"notifications"` + SignOn struct { + DefaultRelayState string `json:"defaultRelayState"` + SsoAcsURL string `json:"ssoAcsUrl"` + IdpIssuer string `json:"idpIssuer"` + Audience string `json:"audience"` + Recipient string `json:"recipient"` + Destination string `json:"destination"` + SubjectNameIDTemplate string `json:"subjectNameIdTemplate"` + SubjectNameIDFormat string `json:"subjectNameIdFormat"` + ResponseSigned bool `json:"responseSigned"` + AssertionSigned bool `json:"assertionSigned"` + SignatureAlgorithm string `json:"signatureAlgorithm"` + DigestAlgorithm string `json:"digestAlgorithm"` + HonorForceAuthn bool `json:"honorForceAuthn"` + AuthnContextClassRef string `json:"authnContextClassRef"` + SpIssuer interface{} `json:"spIssuer"` + RequestCompressed bool `json:"requestCompressed"` + AttributeStatements []interface{} `json:"attributeStatements"` + } `json:"signOn"` + } `json:"settings"` + Links struct { + Logo []struct { + Name string `json:"name"` + Href string `json:"href"` + Type string `json:"type"` + } `json:"logo"` + AppLinks []struct { + Name string `json:"name"` + Href string `json:"href"` + Type string `json:"type"` + } `json:"appLinks"` + Help struct { + Href string `json:"href"` + Type string `json:"type"` + } `json:"help"` + Users struct { + Href string `json:"href"` + } `json:"users"` + Deactivate struct { + Href string `json:"href"` + } `json:"deactivate"` + Groups struct { + Href string `json:"href"` + } `json:"groups"` + Metadata struct { + Href string `json:"href"` + Type string `json:"type"` + } `json:"metadata"` + } `json:"_links"` +} + +func (a App) String() string { + // return Stringify(g) + return fmt.Sprintf("App:(ID: {%v} - Name: {%v})\n", a.ID, a.Name) +} + +// GetByID gets a group from OKTA by the Gropu ID. An error is returned if the group is not found +func (a *AppsService) GetByID(appID string) (*App, *Response, error) { + + u := fmt.Sprintf("apps/%v", appID) + req, err := a.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, nil, err + } + + app := new(App) + + resp, err := a.client.Do(req, app) + + if err != nil { + return nil, resp, err + } + + return app, resp, err +} + +type AppUser struct { + ID string `json:"id"` + ExternalID string `json:"externalId"` + Created time.Time `json:"created"` + LastUpdated time.Time `json:"lastUpdated"` + Scope string `json:"scope"` + Status string `json:"status"` + StatusChanged *time.Time `json:"statusChanged"` + PasswordChanged *time.Time `json:"passwordChanged"` + SyncState string `json:"syncState"` + LastSync *time.Time `json:"lastSync"` + Credentials struct { + UserName string `json:"userName"` + Password struct { + } `json:"password"` + } `json:"credentials"` + Profile struct { + SecondEmail interface{} `json:"secondEmail"` + LastName string `json:"lastName"` + MobilePhone interface{} `json:"mobilePhone"` + Email string `json:"email"` + SalesforceGroups []string `json:"salesforceGroups"` + Role string `json:"role"` + FirstName string `json:"firstName"` + Profile string `json:"profile"` + } `json:"profile"` + Links struct { + App struct { + Href string `json:"href"` + } `json:"app"` + User struct { + Href string `json:"href"` + } `json:"user"` + } `json:"_links"` +} + +// GetUsers returns the members in an App +// Pass in an optional AppFilterOptions struct to filter the results +// The Users in the app are returned +func (a *AppsService) GetUsers(appID string, opt *AppFilterOptions) (appUsers []AppUser, resp *Response, err error) { + + pagesRetreived := 0 + var u string + if opt.NextURL != nil { + u = opt.NextURL.String() + } else { + u = fmt.Sprintf("apps/%v/users", appID) + + if opt.Limit == 0 { + opt.Limit = defaultLimit + } + + u, _ = addOptions(u, opt) + } + + req, err := a.client.NewRequest("GET", u, nil) + + if err != nil { + fmt.Printf("____ERROR HERE\n") + return nil, nil, err + } + resp, err = a.client.Do(req, &appUsers) + + if err != nil { + fmt.Printf("____ERROR HERE 2\n") + 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 []AppUser + pageOpts := new(AppFilterOptions) + pageOpts.NextURL = resp.NextURL + pageOpts.Limit = opt.Limit + pageOpts.NumberOfPages = 1 + + userPage, resp, err = a.GetUsers(appID, pageOpts) + + if err != nil { + return appUsers, resp, err + } else { + appUsers = append(appUsers, userPage...) + pagesRetreived++ + } + } else { + break + } + + } + } + + return appUsers, resp, err +} diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/factors.go b/vendor/github.com/chrismalek/oktasdk-go/okta/factors.go new file mode 100644 index 000000000000..fc926f91aa38 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/factors.go @@ -0,0 +1,8 @@ +package okta + +const ( + // MFAStatusActive is a constant to represent OKTA User State returned by the API + MFAStatusActive = "ACTIVE" + // MFAStatusPending is a user MFA Status of NOT Active + MFAStatusPending = "PENDING_ACTIVATION" +) diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/groups.go b/vendor/github.com/chrismalek/oktasdk-go/okta/groups.go new file mode 100644 index 000000000000..5c9b58086238 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/groups.go @@ -0,0 +1,306 @@ +package okta + +import ( + "errors" + "fmt" + "net/url" + "time" +) + +const ( + // GroupTypeOKTA - group type constant for an OKTA Mastered Group + GroupTypeOKTA = "OKTA_GROUP" + // GroupTypeBuiltIn - group type constant for a Built in OKTA groups + GroupTypeBuiltIn = "BUILT_IN" + // GroupTypeApp -- group type constant for app mastered group + GroupTypeApp = "APP_GROUP" + + groupTypeFilter = "type" + groupNameFilter = "q" + groupLastMembershipUpdatedFilter = "lastMembershipUpdated" + groupLastUpdatedFilter = "lastUpdated" +) + +// GroupsService handles communication with the Groups data related +// methods of the OKTA API. +type GroupsService service + +// Group represents the Group Object from the OKTA API +type Group struct { + ID string `json:"id"` + Created time.Time `json:"created"` + LastUpdated time.Time `json:"lastUpdated"` + LastMembershipUpdated time.Time `json:"lastMembershipUpdated"` + ObjectClass []string `json:"objectClass"` + Type string `json:"type"` + Profile struct { + Name string `json:"name"` + Description string `json:"description"` + SamAccountName string `json:"samAccountName"` + Dn string `json:"dn"` + WindowsDomainQualifiedName string `json:"windowsDomainQualifiedName"` + ExternalID string `json:"externalId"` + } `json:"profile"` + Links struct { + Logo []struct { + Name string `json:"name"` + Href string `json:"href"` + Type string `json:"type"` + } `json:"logo"` + Users struct { + Href string `json:"href"` + } `json:"users"` + Apps struct { + Href string `json:"href"` + } `json:"apps"` + } `json:"_links"` +} + +// GroupFilterOptions is used to generate a "Filter" to search for different groups +// The values here coorelate to API Search paramgters on the group API +type GroupFilterOptions struct { + // 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:"-"` + Limit int `url:"limit,omitempty"` + + NameStartsWith string `url:"q,omitempty"` + GroupTypeEqual string `url:"-"` + + LastUpdated dateFilter `url:"-"` + LastMembershipUpdated dateFilter `url:"-"` +} + +func (g Group) String() string { + // return Stringify(g) + return fmt.Sprintf("Group:(ID: {%v} - Type: {%v} - Group Name: {%v})\n", g.ID, g.Type, g.Profile.Name) +} + +// ListWithFilter - Method to list groups with different filter options. +// Pass in a GroupFilterOptions to specify filters. Values in that struct will turn into Query parameters +func (g *GroupsService) ListWithFilter(opt *GroupFilterOptions) ([]Group, *Response, error) { + + var u string + var err error + + pagesRetreived := 0 + if opt.NextURL != nil { + u = opt.NextURL.String() + } else { + if opt.GroupTypeEqual != "" { + opt.FilterString = appendToFilterString(opt.FilterString, groupTypeFilter, FilterEqualOperator, opt.GroupTypeEqual) + } + + // if opt.NameStartsWith != "" { + // opt.FilterString = appendToFilterString(opt.FilterString, groupNameFilter, filterEqualOperator, opt.NameStartsWith) + // } + if (!opt.LastMembershipUpdated.Value.IsZero()) && (opt.LastMembershipUpdated.Operator != "") { + opt.FilterString = appendToFilterString(opt.FilterString, groupLastMembershipUpdatedFilter, opt.LastMembershipUpdated.Operator, opt.LastMembershipUpdated.Value.UTC().Format(oktaFilterTimeFormat)) + } + + if (!opt.LastUpdated.Value.IsZero()) && (opt.LastUpdated.Operator != "") { + opt.FilterString = appendToFilterString(opt.FilterString, groupLastUpdatedFilter, opt.LastUpdated.Operator, opt.LastUpdated.Value.UTC().Format(oktaFilterTimeFormat)) + } + + if opt.Limit == 0 { + opt.Limit = defaultLimit + } + u, err = addOptions("groups", opt) + if err != nil { + return nil, nil, err + } + } + + req, err := g.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + groups := make([]Group, 1) + resp, err := g.client.Do(req, &groups) + 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 groupPage []Group + pageOption := new(GroupFilterOptions) + pageOption.NextURL = resp.NextURL + pageOption.NumberOfPages = 1 + pageOption.Limit = opt.Limit + + groupPage, resp, err = g.ListWithFilter(pageOption) + if err != nil { + return groups, resp, err + } else { + groups = append(groups, groupPage...) + pagesRetreived++ + } + } else { + break + } + } + } + return groups, resp, err +} + +// GetByID gets a group from OKTA by the Gropu ID. An error is returned if the group is not found +func (g *GroupsService) GetByID(groupID string) (*Group, *Response, error) { + + u := fmt.Sprintf("groups/%v", groupID) + req, err := g.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, nil, err + } + + group := new(Group) + + resp, err := g.client.Do(req, group) + + if err != nil { + return nil, resp, err + } + + return group, resp, err +} + +// GetUsers returns the members in a group +// Pass in an optional GroupFilterOptions struct to filter the results +// The Users in the group are returned +func (g *GroupsService) GetUsers(groupID string, opt *GroupUserFilterOptions) (users []User, resp *Response, err error) { + pagesRetreived := 0 + var u string + if opt.NextURL != nil { + u = opt.NextURL.String() + } else { + u = fmt.Sprintf("groups/%v/users", groupID) + + if opt.Limit == 0 { + opt.Limit = defaultLimit + } + + u, _ = addOptions(u, opt) + } + + req, err := g.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, nil, err + } + resp, err = g.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 + pageOpts := new(GroupUserFilterOptions) + pageOpts.NextURL = resp.NextURL + pageOpts.Limit = opt.Limit + pageOpts.NumberOfPages = 1 + + userPage, resp, err = g.GetUsers(groupID, pageOpts) + if err != nil { + return users, resp, err + } else { + users = append(users, userPage...) + pagesRetreived++ + } + } else { + break + } + + } + } + + return users, resp, err +} + +// Add - Adds an OKTA Mastered Group with name and description. GroupName is required. +func (g *GroupsService) Add(groupName string, groupDescription string) (*Group, *Response, error) { + + if groupName == "" { + return nil, nil, errors.New("groupName parameter is required for ADD") + } + + newGroup := newGroup{} + newGroup.Profile.Name = groupName + newGroup.Profile.Description = groupDescription + + u := fmt.Sprintf("groups") + + req, err := g.client.NewRequest("POST", u, newGroup) + + if err != nil { + return nil, nil, err + } + + group := new(Group) + + resp, err := g.client.Do(req, group) + + if err != nil { + return nil, resp, err + } + + return group, resp, err +} + +// Delete - Delets an OKTA Mastered Group with ID +func (g *GroupsService) Delete(groupID string) (*Response, error) { + + if groupID == "" { + return nil, errors.New("groupID parameter is required for Delete") + } + u := fmt.Sprintf("groups/%v", groupID) + + req, err := g.client.NewRequest("DELETE", u, nil) + + if err != nil { + return nil, err + } + + resp, err := g.client.Do(req, nil) + + if err != nil { + return resp, err + } + + return resp, err +} + +// GroupUserFilterOptions is a struct that you populate which will limit or control group fetches and searches +// The values here will coorelate to the search filtering allowed in the OKTA API. These values are turned into Query Parameters +type GroupUserFilterOptions struct { + Limit int `url:"limit,omitempty"` + NextURL *url.URL `url:"-"` + GetAllPages bool `url:"-"` + NumberOfPages int `url:"-"` +} + +type newGroup struct { + Profile struct { + Name string `json:"name"` + Description string `json:"description"` + } `json:"profile"` +} diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go b/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go new file mode 100644 index 000000000000..322b4410d4c4 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go @@ -0,0 +1,503 @@ +package okta + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "strconv" + "sync" + "time" + + "github.com/google/go-querystring/query" + + "reflect" +) + +const ( + libraryVersion = "1" + userAgent = "oktasdk-go/" + libraryVersion + productionURLFormat = "https://%s.okta.com/api/v1/" + previewProductionURLFormat = "https://%s.oktapreview.com/api/v1/" + headerRateLimit = "X-Rate-Limit-Limit" + headerRateRemaining = "X-Rate-Limit-Remaining" + headerRateReset = "X-Rate-Limit-Reset" + headerOKTARequestID = "X-Okta-Request-Id" + headerAuthorization = "Authorization" + headerAuthorizationFormat = "SSWS %v" + mediaTypeJSON = "application/json" + defaultLimit = 50 + // FilterEqualOperator Filter Operatorid for "equal" + FilterEqualOperator = "eq" + // FilterStartsWithOperator - filter operator for "starts with" + FilterStartsWithOperator = "sw" + // FilterGreaterThanOperator - filter operator for "greater than" + FilterGreaterThanOperator = "gt" + // FilterLessThanOperator - filter operator for "less than" + FilterLessThanOperator = "lt" + + // If the API returns a "X-Rate-Limit-Remaining" header less than this the SDK will either pause + // Or throw RateLimitError depending on the client.PauseOnRateLimit value + defaultRateRemainingFloor = 100 +) + +// A Client manages communication with the API. +type Client struct { + clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func. + client *http.Client // HTTP client used to communicate with the API. + + // Base URL for API requests. + // This will be built automatically based on inputs to NewClient + // If needed you can override this if needed (your URL is not *.okta.com or *.oktapreview.com) + BaseURL *url.URL + + // User agent used when communicating with the GitHub API. + UserAgent string + + apiKey string + authorizationHeaderValue string + PauseOnRateLimit bool + + // RateRemainingFloor - If the API returns a "X-Rate-Limit-Remaining" header less than this the SDK will either pause + // Or throw RateLimitError depending on the client.PauseOnRateLimit value. It defaults to 30 + // One client doing too much work can lock out all API Access for every other client + // We are trying to be a "good API User Citizen" + RateRemainingFloor int + + rateMu sync.Mutex + mostRecentRate Rate + + Limit int + // mostRecent rateLimitCategory + + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // Services used for talking to different parts of the API. + // Service for Working with Users + Users *UsersService + + // Service for Working with Groups + Groups *GroupsService + + // Service for Working with Apps + Apps *AppsService +} + +type service struct { + client *Client +} + +// NewClient returns a new OKTA API client. If a nil httpClient is +// provided, http.DefaultClient will be used. +func NewClient(httpClient *http.Client, orgName string, apiToken string, isProduction bool) *Client { + if httpClient == nil { + httpClient = http.DefaultClient + } + + var baseURL *url.URL + if isProduction { + baseURL, _ = url.Parse(fmt.Sprintf(productionURLFormat, orgName)) + } else { + baseURL, _ = url.Parse(fmt.Sprintf(previewProductionURLFormat, orgName)) + + } + + c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent} + c.PauseOnRateLimit = true // If rate limit found it will block until that time. If false then Error will be returned + c.authorizationHeaderValue = fmt.Sprintf(headerAuthorizationFormat, apiToken) + c.apiKey = apiToken + c.Limit = defaultLimit + c.RateRemainingFloor = defaultRateRemainingFloor + c.common.client = c + + c.Users = (*UsersService)(&c.common) + c.Groups = (*GroupsService)(&c.common) + c.Apps = (*AppsService)(&c.common) + return c +} + +// Rate represents the rate limit for the current client. +type Rate struct { + // The number of requests per minute the client is currently limited to. + RatePerMinuteLimit int + + // The number of remaining requests the client can make this minute + Remaining int + + // The time at which the current rate limit will reset. + ResetTime time.Time +} + +// Response is a OKTA API response. This wraps the standard http.Response +// returned from OKTA and provides convenient access to things like +// pagination links. +type Response struct { + *http.Response + + // These fields provide the page values for paginating through a set of + // results. + + NextURL *url.URL + // PrevURL *url.URL + SelfURL *url.URL + OKTARequestID string + Rate +} + +// newResponse creates a new Response for the provided http.Response. +func newResponse(r *http.Response) *Response { + response := &Response{Response: r} + + response.OKTARequestID = r.Header.Get(headerOKTARequestID) + + response.populatePaginationURLS() + response.Rate = parseRate(r) + return response +} + +// populatePageValues parses the HTTP Link response headers and populates the +// various pagination link values in the Response. + +// OKTA LINK Header takes this form: +// Link: ; rel="next", +// ; rel="self" + +func (r *Response) populatePaginationURLS() { + + for k, v := range r.Header { + + if k == "Link" { + nextRegex := regexp.MustCompile(`<(.*?)>; rel="next"`) + // prevRegex := regexp.MustCompile(`<(.*?)>; rel="prev"`) + selfRegex := regexp.MustCompile(`<(.*?)>; rel="self"`) + + for _, linkValue := range v { + nextLinkMatch := nextRegex.FindStringSubmatch(linkValue) + if len(nextLinkMatch) != 0 { + r.NextURL, _ = url.Parse(nextLinkMatch[1]) + } + selfLinkMatch := selfRegex.FindStringSubmatch(linkValue) + if len(selfLinkMatch) != 0 { + r.SelfURL, _ = url.Parse(selfLinkMatch[1]) + } + // prevLinkMatch := prevRegex.FindStringSubmatch(linkValue) + // if len(prevLinkMatch) != 0 { + // r.PrevURL, _ = url.Parse(prevLinkMatch[1]) + // } + } + } + } + +} + +// parseRate parses the rate related headers. +func parseRate(r *http.Response) Rate { + var rate Rate + + if limit := r.Header.Get(headerRateLimit); limit != "" { + rate.RatePerMinuteLimit, _ = strconv.Atoi(limit) + } + if remaining := r.Header.Get(headerRateRemaining); remaining != "" { + rate.Remaining, _ = strconv.Atoi(remaining) + } + if reset := r.Header.Get(headerRateReset); reset != "" { + if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { + rate.ResetTime = time.Unix(v, 0) + } + } + return rate +} + +// Do sends an API request and returns the API response. The API response is +// JSON decoded and stored in the value pointed to by v, or returned as an +// error if an API error has occurred. If v implements the io.Writer +// interface, the raw response body will be written to v, without attempting to +// first decode it. If rate limit is exceeded and reset time is in the future, +// Do returns rate immediately without making a network API call. +func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { + + // If we've hit rate limit, don't make further requests before Reset time. + if err := c.checkRateLimitBeforeDo(req); err != nil { + return nil, err + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + defer func() { + // Drain up to 512 bytes and close the body to let the Transport reuse the connection + io.CopyN(ioutil.Discard, resp.Body, 512) + resp.Body.Close() + }() + + response := newResponse(resp) + + c.rateMu.Lock() + c.mostRecentRate.RatePerMinuteLimit = response.Rate.RatePerMinuteLimit + c.mostRecentRate.Remaining = response.Rate.Remaining + c.mostRecentRate.ResetTime = response.Rate.ResetTime + c.rateMu.Unlock() + + err = CheckResponse(resp) + if err != nil { + // even though there was an error, we still return the response + // in case the caller wants to inspect it further + // fmt.Printf("Error after sdk.Do return\n") + + return response, err + } + + if v != nil { + if w, ok := v.(io.Writer); ok { + io.Copy(w, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(v) + if err == io.EOF { + err = nil // ignore EOF errors caused by empty response body + } + } + } + + return response, err +} + +// checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from +// current client state in order to quickly check if *RateLimitError can be immediately returned +// from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. +// Otherwise it returns nil, and Client.Do should proceed normally. +// http://developer.okta.com/docs/api/getting_started/design_principles.html#rate-limiting +func (c *Client) checkRateLimitBeforeDo(req *http.Request) error { + + c.rateMu.Lock() + mostRecentRate := c.mostRecentRate + c.rateMu.Unlock() + // fmt.Printf("checkRateLimitBeforeDo: \t Remaining = %d, \t ResetTime = %s\n", mostRecentRate.Remaining, mostRecentRate.ResetTime.String()) + if !mostRecentRate.ResetTime.IsZero() && mostRecentRate.Remaining < c.RateRemainingFloor && time.Now().Before(mostRecentRate.ResetTime) { + + if c.PauseOnRateLimit { + // If rate limit is hitting threshold then pause until the rate limit resets + // This behavior is controlled by the client PauseOnRateLimit value + // fmt.Printf("checkRateLimitBeforeDo: \t ***pause**** \t Time Now = %s \tPause After = %s\n", time.Now().String(), mostRecentRate.ResetTime.Sub(time.Now().Add(2*time.Second)).String()) + <-time.After(mostRecentRate.ResetTime.Sub(time.Now().Add(2 * time.Second))) + } else { + // fmt.Printf("checkRateLimitBeforeDo: \t ***error****\n") + + return &RateLimitError{ + Rate: mostRecentRate, + } + } + + } + + return nil +} + +// CheckResponse checks the API response for errors, and returns them if +// present. A response is considered an error if it has a status code outside +// the 200 range. API error responses are expected to have either no response +// body, or a JSON response body that maps to ErrorResponse. Any other +// response body will be silently ignored. +// +// The error type will be *RateLimitError for rate limit exceeded errors, +// and *TwoFactorAuthError for two-factor authentication errors. +// TODO - check un-authorized +func CheckResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil + } + + errorResp := &errorResponse{Response: r} + data, err := ioutil.ReadAll(r.Body) + if err == nil && data != nil { + json.Unmarshal(data, &errorResp.ErrorDetail) + } + switch { + case r.StatusCode == http.StatusTooManyRequests: + + return &RateLimitError{ + Rate: parseRate(r), + Response: r, + ErrorDetail: errorResp.ErrorDetail} + + default: + return errorResp + } + +} + +type apiError struct { + ErrorCode string `json:"errorCode"` + ErrorSummary string `json:"errorSummary"` + ErrorLink string `json:"errorLink"` + ErrorID string `json:"errorId"` + ErrorCauses []struct { + ErrorSummary string `json:"errorSummary"` + } `json:"errorCauses"` +} + +type errorResponse struct { + Response *http.Response // + ErrorDetail apiError +} + +func (r *errorResponse) Error() string { + return fmt.Sprintf("HTTP Method: %v - URL: %v: - HTTP Status Code: %d, OKTA Error Code: %v, OKTA Error Summary: %v, OKTA Error Causes: %v", + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.ErrorDetail.ErrorCode, r.ErrorDetail.ErrorSummary, r.ErrorDetail.ErrorCauses) +} + +// RateLimitError occurs when OKTA returns 429 "Too Many Requests" response with a rate limit +// remaining value of 0, and error message starts with "API rate limit exceeded for ". +type RateLimitError struct { + Rate Rate // Rate specifies last known rate limit for the client + ErrorDetail apiError + Response *http.Response // +} + +func (r *RateLimitError) Error() string { + + return fmt.Sprintf("rate reset in %v", r.Rate.ResetTime.Sub(time.Now())) + +} + +// Code stolen from Github api libary +// Stringify attempts to create a reasonable string representation of types in +// the library. It does things like resolve pointers to their values +// and omits struct fields with nil values. +func stringify(message interface{}) string { + var buf bytes.Buffer + v := reflect.ValueOf(message) + stringifyValue(&buf, v) + return buf.String() +} + +// stringifyValue was heavily inspired by the goprotobuf library. + +func stringifyValue(w io.Writer, val reflect.Value) { + if val.Kind() == reflect.Ptr && val.IsNil() { + w.Write([]byte("")) + return + } + + v := reflect.Indirect(val) + + switch v.Kind() { + case reflect.String: + fmt.Fprintf(w, `"%s"`, v) + case reflect.Slice: + w.Write([]byte{'['}) + for i := 0; i < v.Len(); i++ { + if i > 0 { + w.Write([]byte{' '}) + } + + stringifyValue(w, v.Index(i)) + } + + w.Write([]byte{']'}) + return + case reflect.Struct: + if v.Type().Name() != "" { + w.Write([]byte(v.Type().String())) + } + w.Write([]byte{'{'}) + + var sep bool + for i := 0; i < v.NumField(); i++ { + fv := v.Field(i) + if fv.Kind() == reflect.Ptr && fv.IsNil() { + continue + } + if fv.Kind() == reflect.Slice && fv.IsNil() { + continue + } + + if sep { + w.Write([]byte(", ")) + } else { + sep = true + } + + w.Write([]byte(v.Type().Field(i).Name)) + w.Write([]byte{':'}) + stringifyValue(w, fv) + } + + w.Write([]byte{'}'}) + default: + if v.CanInterface() { + fmt.Fprint(w, v.Interface()) + } + } +} + +// NewRequest creates an API request. A relative URL can be provided in urlStr, +// in which case it is resolved relative to the BaseURL of the Client. +// Relative URLs should always be specified without a preceding slash. If +// specified, the value pointed to by body is JSON encoded and included as the +// request body. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + rel, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + u := c.BaseURL.ResolveReference(rel) + + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + req.Header.Set(headerAuthorization, fmt.Sprintf(headerAuthorizationFormat, c.apiKey)) + + if body != nil { + req.Header.Set("Content-Type", mediaTypeJSON) + } + + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + return req, nil +} + +// addOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +func addOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + u.RawQuery = qs.Encode() + return u.String(), nil +} + +type dateFilter struct { + Value time.Time + Operator string +} diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/users.go b/vendor/github.com/chrismalek/oktasdk-go/okta/users.go new file mode 100644 index 000000000000..73448f50326d --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/users.go @@ -0,0 +1,600 @@ +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:"-,omitempty"` + Groups []Group `json:"-,omitempty"` +} + +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"` +} + +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 + } + // TODO: If user has more than 200 groups this will only return those first 200 + resp, err := s.client.Do(req, &user.Groups) + if err != nil { + return resp, err + } + + 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 + } else { + 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 +} diff --git a/vendor/github.com/sstarcher/go-okta/README.md b/vendor/github.com/sstarcher/go-okta/README.md deleted file mode 100644 index 28e880c3ade5..000000000000 --- a/vendor/github.com/sstarcher/go-okta/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Okta golang client -================ - -[![CircleCI](https://circleci.com/gh/sstarcher/job-reaper.svg?style=svg)](https://circleci.com/gh/sstarcher/go-okta) - - -Basic Okta HTTP client - - diff --git a/vendor/github.com/sstarcher/go-okta/api.go b/vendor/github.com/sstarcher/go-okta/api.go deleted file mode 100644 index 1236f07445c0..000000000000 --- a/vendor/github.com/sstarcher/go-okta/api.go +++ /dev/null @@ -1,123 +0,0 @@ -package okta - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" -) - -// Client to access okta -type Client struct { - client *http.Client - org string - Url string - ApiToken string -} - -// errorResponse is an error wrapper for the okta response -type errorResponse struct { - HTTPCode int - Response ErrorResponse - Endpoint string -} - -func (e *errorResponse) Error() string { - return fmt.Sprintf("Error hitting api endpoint %s %s", e.Endpoint, e.Response.ErrorCode) -} - -// NewClient object for calling okta -func NewClient(org string) *Client { - client := Client{ - client: &http.Client{}, - org: org, - Url: "okta.com", - } - - return &client -} - -// Authenticate with okta using username and password -func (c *Client) Authenticate(username, password string) (*AuthnResponse, error) { - var request = &AuthnRequest{ - Username: username, - Password: password, - } - - var response = &AuthnResponse{} - err := c.call("authn", "POST", request, response) - return response, err -} - -// Session takes a session token and always fails -func (c *Client) Session(sessionToken string) (*SessionResponse, error) { - var request = &SessionRequest{ - SessionToken: sessionToken, - } - - var response = &SessionResponse{} - err := c.call("sessions", "POST", request, response) - return response, err -} - -// User takes a user id and returns data about that user -func (c *Client) User(userID string) (*User, error) { - - var response = &User{} - err := c.call("users/"+userID, "GET", nil, response) - return response, err -} - -// Groups takes a user id and returns the groups the user belongs to -func (c *Client) Groups(userID string) (*Groups, error) { - - var response = &Groups{} - err := c.call("users/"+userID+"/groups", "GET", nil, response) - return response, err -} - -func (c *Client) call(endpoint, method string, request, response interface{}) error { - data, _ := json.Marshal(request) - - var url = "https://" + c.org + "." + c.Url + "/api/v1/" + endpoint - req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) - if err != nil { - return err - } - - req.Header.Add("Accept", `application/json`) - req.Header.Add("Content-Type", `application/json`) - if c.ApiToken != "" { - req.Header.Add("Authorization", "SSWS "+c.ApiToken) - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - if resp.StatusCode == http.StatusOK { - err := json.Unmarshal(body, &response) - if err != nil { - return err - } - } else { - var errors ErrorResponse - err = json.Unmarshal(body, &errors) - - return &errorResponse{ - HTTPCode: resp.StatusCode, - Response: errors, - Endpoint: url, - } - } - - return nil -} diff --git a/vendor/github.com/sstarcher/go-okta/authn.go b/vendor/github.com/sstarcher/go-okta/authn.go deleted file mode 100644 index f0cc8eb9ca3d..000000000000 --- a/vendor/github.com/sstarcher/go-okta/authn.go +++ /dev/null @@ -1,45 +0,0 @@ -package okta - -import ( - "time" -) - -type ErrorResponse struct { - ErrorCode string `json:"errorCode"` - ErrorSummary string `json:"errorSummary"` - ErrorLink string `json:"errorLink"` - ErrorID string `json:"errorId"` - ErrorCauses []struct { - ErrorSummary string `json:"errorSummary"` - } `json:"errorCauses"` -} - -type AuthnRequest struct { - Username string `json:"username"` - Password string `json:"password"` - RelayState string `json:"relayState"` - Options struct { - MultiOptionalFactorEnroll bool `json:"multiOptionalFactorEnroll"` - WarnBeforePasswordExpired bool `json:"warnBeforePasswordExpired"` - } `json:"options"` -} - -type AuthnResponse struct { - ExpiresAt time.Time `json:"expiresAt"` - Status string `json:"status"` - RelayState string `json:"relayState"` - SessionToken string `json:"sessionToken"` - Embedded struct { - User struct { - ID string `json:"id"` - PasswordChanged time.Time `json:"passwordChanged"` - Profile struct { - Login string `json:"login"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Locale string `json:"locale"` - TimeZone string `json:"timeZone"` - } `json:"profile"` - } `json:"user"` - } `json:"_embedded"` -} diff --git a/vendor/github.com/sstarcher/go-okta/circleci.yml b/vendor/github.com/sstarcher/go-okta/circleci.yml deleted file mode 100644 index 20db0cf2b9b9..000000000000 --- a/vendor/github.com/sstarcher/go-okta/circleci.yml +++ /dev/null @@ -1,19 +0,0 @@ -machine: - environment: - GOPATH: "${HOME}/.go_workspace" - IMPORT_PATH: "${GOPATH}/src/github.com/${CIRCLE_PROJECT_USERNAME}" - APP_PATH: "${IMPORT_PATH}/${CIRCLE_PROJECT_REPONAME}" - -dependencies: - override: - - sudo add-apt-repository ppa:masterminds/glide -y - - sudo apt-get update - - sudo apt-get install glide -y - -test: - pre: - - mkdir -p "$IMPORT_PATH" - - ln -sf "$(pwd)" "${APP_PATH}" - - cd "${APP_PATH}" && glide install - override: - - cd "${APP_PATH}" && go test -cover $(glide nv) diff --git a/vendor/github.com/sstarcher/go-okta/glide.lock b/vendor/github.com/sstarcher/go-okta/glide.lock deleted file mode 100644 index ea836401749d..000000000000 --- a/vendor/github.com/sstarcher/go-okta/glide.lock +++ /dev/null @@ -1,4 +0,0 @@ -hash: acc035e4a3e5e3ed975f4233cc66fdbf3af5eb7bc2b5b337a26f730abf86e4b7 -updated: 2016-09-28T11:14:46.44318819-04:00 -imports: [] -testImports: [] diff --git a/vendor/github.com/sstarcher/go-okta/glide.yaml b/vendor/github.com/sstarcher/go-okta/glide.yaml deleted file mode 100644 index 79792630af75..000000000000 --- a/vendor/github.com/sstarcher/go-okta/glide.yaml +++ /dev/null @@ -1,2 +0,0 @@ -package: github.com/sstarcher/go-okta -import: [] diff --git a/vendor/github.com/sstarcher/go-okta/sessions.go b/vendor/github.com/sstarcher/go-okta/sessions.go deleted file mode 100644 index b974683f1553..000000000000 --- a/vendor/github.com/sstarcher/go-okta/sessions.go +++ /dev/null @@ -1,46 +0,0 @@ -package okta - -import ( - "time" -) - -type SessionRequest struct { - SessionToken string `json:"sessionToken"` -} - -type SessionResponse struct { - ID string `json:"id"` - Login string `json:"login"` - UserID string `json:"userId"` - ExpiresAt time.Time `json:"expiresAt"` - Status string `json:"status"` - LastPasswordVerification time.Time `json:"lastPasswordVerification"` - LastFactorVerification interface{} `json:"lastFactorVerification"` - Amr []string `json:"amr"` - Idp struct { - ID string `json:"id"` - Type string `json:"type"` - } `json:"idp"` - MfaActive bool `json:"mfaActive"` - Links struct { - Self struct { - Href string `json:"href"` - Hints struct { - Allow []string `json:"allow"` - } `json:"hints"` - } `json:"self"` - Refresh struct { - Href string `json:"href"` - Hints struct { - Allow []string `json:"allow"` - } `json:"hints"` - } `json:"refresh"` - User struct { - Name string `json:"name"` - Href string `json:"href"` - Hints struct { - Allow []string `json:"allow"` - } `json:"hints"` - } `json:"user"` - } `json:"_links"` -} diff --git a/vendor/github.com/sstarcher/go-okta/users.go b/vendor/github.com/sstarcher/go-okta/users.go deleted file mode 100644 index 850d4b97cf6f..000000000000 --- a/vendor/github.com/sstarcher/go-okta/users.go +++ /dev/null @@ -1,83 +0,0 @@ -package okta - -import ( - "time" -) - -type User struct { - ID string `json:"id"` - Status string `json:"status"` - Created *time.Time `json:"created"` - Activated *time.Time `json:"activated"` - StatusChanged *time.Time `json:"statusChanged"` - LastLogin *time.Time `json:"lastLogin"` - LastUpdated *time.Time `json:"lastUpdated"` - PasswordChanged *time.Time `json:"passwordChanged"` - Profile struct { - Login string `json:"login"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - NickName string `json:"nickName"` - DisplayName string `json:"displayName"` - Email string `json:"email"` - SecondEmail string `json:"secondEmail"` - ProfileURL string `json:"profileUrl"` - PreferredLanguage string `json:"preferredLanguage"` - UserType string `json:"userType"` - Organization string `json:"organization"` - Title string `json:"title"` - Division string `json:"division"` - Department string `json:"department"` - CostCenter string `json:"costCenter"` - EmployeeNumber string `json:"employeeNumber"` - MobilePhone string `json:"mobilePhone"` - PrimaryPhone string `json:"primaryPhone"` - StreetAddress string `json:"streetAddress"` - City string `json:"city"` - State string `json:"state"` - ZipCode string `json:"zipCode"` - CountryCode string `json:"countryCode"` - } `json:"profile"` - Credentials struct { - Password struct { - } `json:"password"` - RecoveryQuestion struct { - Question string `json:"question"` - } `json:"recovery_question"` - Provider struct { - Type string `json:"type"` - Name string `json:"name"` - } `json:"provider"` - } `json:"credentials"` - Links struct { - ResetPassword struct { - Href string `json:"href"` - } `json:"resetPassword"` - ResetFactors struct { - Href string `json:"href"` - } `json:"resetFactors"` - ExpirePassword struct { - Href string `json:"href"` - } `json:"expirePassword"` - ForgotPassword struct { - Href string `json:"href"` - } `json:"forgotPassword"` - ChangeRecoveryQuestion struct { - Href string `json:"href"` - } `json:"changeRecoveryQuestion"` - Deactivate struct { - Href string `json:"href"` - } `json:"deactivate"` - ChangePassword struct { - Href string `json:"href"` - } `json:"changePassword"` - } `json:"_links"` -} - -type Groups []struct { - ID string `json:"id"` - Profile struct { - Name string `json:"name"` - Description string `json:"description"` - } `json:"profile"` -} diff --git a/vendor/vendor.json b/vendor/vendor.json index bc75586e09a4..f507f1aa7d14 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -446,6 +446,12 @@ "revision": "61153c768f31ee5f130071d08fc82b85208528de", "revisionTime": "2017-07-11T19:02:43Z" }, + { + "checksumSHA1": "QZtBo/fc3zeQFxPFgPVMyDiw70M=", + "path": "github.com/chrismalek/oktasdk-go/okta", + "revision": "7d4ce0a254ec5f9eda3397523f6cf183e1d46c5e", + "revisionTime": "2017-02-07T05:01:14Z" + }, { "checksumSHA1": "Ymghbn2vkOAdT9rNQxKR2qNuxtA=", "path": "github.com/circonus-labs/circonus-gometrics", @@ -1358,12 +1364,6 @@ "revision": "e49c59d69b89746796d48156991108c80faf1d7d", "revisionTime": "2017-07-18T14:06:42Z" }, - { - "checksumSHA1": "7b7psq20O8IOCr885W2Ld6a3KTc=", - "path": "github.com/sstarcher/go-okta", - "revision": "64b3cb9e3a7b6d0c4e4432576c873e492d152666", - "revisionTime": "2017-04-28T20:44:25Z" - }, { "checksumSHA1": "9Zw986fuQM/hCoVd8vmHoSM+8sU=", "path": "github.com/ugorji/go/codec", diff --git a/website/source/api/auth/okta/index.html.md b/website/source/api/auth/okta/index.html.md index 6eff597b59cb..efa149e16408 100644 --- a/website/source/api/auth/okta/index.html.md +++ b/website/source/api/auth/okta/index.html.md @@ -27,11 +27,12 @@ distinction between the `create` and `update` capabilities inside ACL policies. ### Parameters -- `organization` `(string: )` - Okta organization to authenticate - against. -- `token` `(string: "")` - Okta admin API token. -- `base_url` `(string: "")` - The API endpoint to use. Useful if you are using - Okta development accounts. +- `org_name` `(string: )` - Name of the organization to be used in the + Okta API. +- `api_token` `(string: )` - Okta API key. +- `production` `(bool: true)` - If set, production API URL prefix will be used + to communicate with Okta and if not set, a preview production API URL prefix + will be used. Defaults to true. - `ttl` `(string: "")` - Duration after which authentication will be expired. - `max_ttl` `(string: "")` - Maximum duration after which authentication will be expired. @@ -40,8 +41,8 @@ distinction between the `create` and `update` capabilities inside ACL policies. ```json { - "organization": "example", - "token": "abc123" + "org_name": "example", + "api_token": "abc123" } ``` @@ -80,9 +81,9 @@ $ curl \ "lease_duration": 0, "renewable": false, "data": { - "organization": "example", - "token": "abc123", - "base_url": "", + "org_name": "example", + "api_token": "abc123", + "production": true, "ttl": "", "max_ttl": "" },