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 } 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 } 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 - Deletes 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 } // AddUserToGroup - Adds a user to a group. func (g *GroupsService) AddUserToGroup(groupID string, userID string) (*Response, error) { if groupID == "" { return nil, errors.New("groupID parameter is required for Delete") } if userID == "" { return nil, errors.New("groupID parameter is required for Delete") } u := fmt.Sprintf("groups/%v/users/%v", groupID, userID) req, err := g.client.NewRequest("PUT", u, nil) if err != nil { return nil, err } resp, err := g.client.Do(req, nil) if err != nil { return resp, err } return resp, err } // RemoveUserFromGroup - Removes a user to a group. func (g *GroupsService) RemoveUserFromGroup(groupID string, userID string) (*Response, error) { if groupID == "" { return nil, errors.New("groupID parameter is required for Delete") } if userID == "" { return nil, errors.New("groupID parameter is required for Delete") } u := fmt.Sprintf("groups/%v/users/%v", groupID, userID) 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"` }