Skip to content

Commit

Permalink
Add rule command (#1)
Browse files Browse the repository at this point in the history
- Add rule command to print out all groups related to a rule
  • Loading branch information
popsu authored Dec 19, 2023
1 parent 5442509 commit dc365e1
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 5 deletions.
135 changes: 135 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"context"
"fmt"
"regexp"
"sort"
"strings"

Expand Down Expand Up @@ -214,3 +215,137 @@ func (oi *OIClient) getUsersInGroupsUnion(groups []string) ([]string, error) {

return users, nil
}

type OktaGroup struct {
Name string `json:"name"`
ID string `json:"id"`
}

func (oi *OIClient) ListGroups() ([]OktaGroup, error) {
var oktaGroups []OktaGroup

addToGroup := func(g []*okta.Group) {
for _, group := range g {
oktaGroups = append(oktaGroups, OktaGroup{
Name: group.Profile.Name,
ID: group.Id,
})
}
}

qp := query.NewQueryParams() // default limit per docs is 10_000

respGroups, resp, err := oi.c.Group.ListGroups(context.TODO(), qp)
if err != nil {
return nil, err
}
addToGroup(respGroups)

// Pagination
for resp.HasNextPage() {
respGroups = nil
resp, err = resp.Next(context.TODO(), &respGroups)
if err != nil {
return nil, err
}

addToGroup(respGroups)
}

return oktaGroups, nil
}

type OktaGroupRule struct {
Name string `json:"name"`
ID string `json:"id"`
DestinationGroupID string `json:"destination_group_id"`
SourceGroupIDs []string `json:"source_group_ids"`
// Currently we don't support Users assigned via rule, but rather manually to the group
// SourceUserIDs []string `json:"user_ids"`
}

func (oi *OIClient) ListGroupRules(searchString string) ([]OktaGroupRule, error) {
var oktaGroupRules []OktaGroupRule

addToGroupRule := func(gr []*okta.GroupRule) error {
for _, groupRule := range gr {
ogr := OktaGroupRule{
Name: groupRule.Name,
ID: groupRule.Id,
DestinationGroupID: groupRule.Actions.AssignUserToGroups.GroupIds[0],
}

if groupRule.Actions == nil || groupRule.Actions.AssignUserToGroups == nil || len(groupRule.Actions.AssignUserToGroups.GroupIds) != 1 {
return fmt.Errorf("group rule %s has no destination group", groupRule.Name)
}
ogr.DestinationGroupID = groupRule.Actions.AssignUserToGroups.GroupIds[0]

if groupRule.Conditions == nil || groupRule.Conditions.Expression == nil {
return fmt.Errorf("group rule %s has no conditions", groupRule.Name)
}
expression := groupRule.Conditions.Expression.Value
ogr.SourceGroupIDs = parseGroupRuleExpression(expression)

oktaGroupRules = append(oktaGroupRules, ogr)
}

return nil
}

opts := []query.ParamOptions{
query.WithLimit(200), // max limit per docs
}
// use search string if not empty
if searchString != "" {
opts = append(opts, query.WithSearch(searchString))
}
qp := query.NewQueryParams(opts...)

groupRules, resp, err := oi.c.Group.ListGroupRules(context.TODO(), qp)
if err != nil {
return nil, err
}

err = addToGroupRule(groupRules)
if err != nil {
return nil, err
}

// pagination
for resp.HasNextPage() {
groupRules = nil
resp, err = resp.Next(context.TODO(), &groupRules)
if err != nil {
return nil, err
}

err = addToGroupRule(groupRules)
if err != nil {
return nil, err
}
}

return oktaGroupRules, nil
}

var reGroupRuleExpression = regexp.MustCompile(`"(\w{20})"`)

// parseGroupRuleExpression parses the expression string from Okta API response
// and returns a slice of group IDs. See TestParseGroupRuleExpression for example input and output.
func parseGroupRuleExpression(expression string) []string {
var groupIDs []string

matches := reGroupRuleExpression.FindAllStringSubmatch(expression, -1)

for _, match := range matches {
if len(match) < 2 {
continue
}

if match[1] != "" {
groupIDs = append(groupIDs, match[1])
}
}

return groupIDs
}
38 changes: 38 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package client

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseGroupRuleExpression(t *testing.T) {
testCases := []struct {
name string
input string
expected []string
}{
{
name: "3 Groups",
input: `"isMemberOfAnyGroup("00g1lghmvirItveA14x7", "00g360hu5bfvaBHP84x7", "00g1l7ll9aGlqnSg24x7")"`,
expected: []string{
"00g1lghmvirItveA14x7",
"00g360hu5bfvaBHP84x7",
"00g1l7ll9aGlqnSg24x7",
},
},
{
name: "1 Group",
input: `isMemberOfAnyGroup("00gar7xacmKf3wNAt4x7")`,
expected: []string{
"00gar7xacmKf3wNAt4x7",
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, parseGroupRuleExpression(tc.input))
})
}
}
132 changes: 132 additions & 0 deletions client/rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package client

import (
"fmt"
"slices"
"strings"
"sync"
)

// PrintGroupRules prints all the group rules that have the searchGroup as either source or destination
func (oi *OIClient) PrintGroupRules(searchGroup string) error {
var wg sync.WaitGroup

var groups []OktaGroup

// TODO use errgroup.Group, it should be suited for this kind of use cases
// https://pkg.go.dev/golang.org/x/sync/errgroup
wg.Add(1)
go func() {
var err error
groups, err = oi.ListGroups()
if err != nil {
panic(err)
}
wg.Done()
}()

var rules []OktaGroupRule

wg.Add(1)
go func() {
var err error
rules, err = oi.ListGroupRules(searchGroup)
if err != nil {
panic(err)
}
wg.Done()
}()

wg.Wait()

// Create a map of groupID -> groupName
groupIDMap := make(map[string]string)
for _, group := range groups {
groupIDMap[group.ID] = group.Name
}

// replace groupID with groupName in rules
for i, rule := range rules {
rule.DestinationGroupID = groupIDMap[rule.DestinationGroupID]
sourceGroupIDs := make([]string, len(rule.SourceGroupIDs))
for i, sourceGroupID := range rule.SourceGroupIDs {
sourceGroupIDs[i] = groupIDMap[sourceGroupID]
}
rule.SourceGroupIDs = sourceGroupIDs

rules[i] = rule
}

groupRulesString := filterRulesToFormatted(searchGroup, rules)
fmt.Println(groupRulesString)

return nil
}

// filterRulesToFormatted filters the rules to only include the ones that have searchGroup as either source or destination
// and formats them in a string that is ready to be printed to terminal
func filterRulesToFormatted(searchGroup string, ogr []OktaGroupRule) string {
var filteredOgr []OktaGroupRule

sourceMaxLength := 0

// Only pick the rules that have searchGroup as either source or destination
for _, o := range ogr {
// is this shouldAdd stuff even needed?
shoulAdd := true

if strings.EqualFold(o.DestinationGroupID, searchGroup) {
shoulAdd = true
}

var wantedSourceGroupValue string
for _, sourceGroupID := range o.SourceGroupIDs {
if strings.EqualFold(sourceGroupID, searchGroup) {
sourceMaxLength = max(sourceMaxLength, len(sourceGroupID))
shoulAdd = true
// we need separate value to make sure Capitalization is proper
wantedSourceGroupValue = sourceGroupID
}
}

if !shoulAdd {
continue
}
// Only add the dependency to/from wantedValue, ignore other rules
if strings.EqualFold(o.DestinationGroupID, searchGroup) {
for _, sourceGroupID := range o.SourceGroupIDs {
sourceMaxLength = max(sourceMaxLength, len(sourceGroupID))
}
// add all
filteredOgr = append(filteredOgr, o)
}
// wantedValue is one of the sourceGroups, drop the other sourceGroups
if strings.EqualFold(wantedSourceGroupValue, searchGroup) {
ogrNew := OktaGroupRule{
DestinationGroupID: o.DestinationGroupID,
SourceGroupIDs: []string{wantedSourceGroupValue},
}
filteredOgr = append(filteredOgr, ogrNew)
}
}

// separate slice for printing so we can get output alphabetically sorted
var printSlice []string

for _, o := range filteredOgr {
for _, sourceGroupID := range o.SourceGroupIDs {
printSlice = append(printSlice, fmt.Sprintf("%-*s -> %s", sourceMaxLength, sourceGroupID, o.DestinationGroupID))
}
}

slices.Sort(printSlice)

var sb strings.Builder

for _, s := range printSlice {
sb.WriteString(s)
sb.WriteString("\n")
}

return sb.String()
}
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ go 1.21.1
require (
github.com/okta/okta-sdk-golang/v2 v2.20.0
github.com/samber/lo v1.38.1
github.com/stretchr/testify v1.7.1
)

require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand Down
15 changes: 13 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@ var (
apiToken = os.Getenv("OKTA_INFO_API_TOKEN")
)

func printHelp() {
fmt.Println("Usage: okta-info <subcommand> <subcommand arguments>")
fmt.Println("Subcommands:")
fmt.Println(" group <group name> - print users in a group")
fmt.Println(" user <user name> - print groups for a user")
fmt.Println(" diff <group1,group2> <group3,group4> - print users in any of groups 1 or 2 but not in groups 3 or 4")
fmt.Println(" rule <group name> - print group rules for a group")
}

func run() error {
// Check which subcommand was provided
if len(os.Args) < 3 {
fmt.Println("Please provide a subcommand and user/group name")
printHelp()
os.Exit(1)
}

Expand Down Expand Up @@ -50,8 +59,10 @@ func run() error {
hideDeprovisioned := false

return oic.PrintGroupDiff(groupsA, groupsB, hideDeprovisioned)
case "rule":
return oic.PrintGroupRules(os.Args[2])
default:
fmt.Println("Invalid subcommand. Valid commands are: group, diff and user")
printHelp()
os.Exit(1)
}
// should not get here ever
Expand Down

0 comments on commit dc365e1

Please sign in to comment.