diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml index a0ed4f9..14f5079 100644 --- a/.github/workflows/goreleaser.yaml +++ b/.github/workflows/goreleaser.yaml @@ -12,18 +12,20 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Set up Go + + - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.21.2 - - - name: Run GoReleaser + go-version: 1.21.6 + + - name: Run tests + run: go test -v ./... + + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: # either 'goreleaser' (default) or 'goreleaser-pro' diff --git a/README.md b/README.md index 3b653d3..62149ea 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,11 @@ OKTA_INFO_API_TOKEN= okta-info diff ``` -4. Query all rules related to a group: +4. Query rules related to a group: - Currently only works for very simple rules that use `isMemberOfAnyGroup` + Currently only works for few rules, so this might not work as expected ```shell - okta-info rules + okta-info rule group # Search using group name + okta-info rule name # Search using rule name ``` diff --git a/client/client.go b/client/client.go index 0f6e2d8..81b22b6 100644 --- a/client/client.go +++ b/client/client.go @@ -328,24 +328,49 @@ func (oi *OIClient) ListGroupRules(searchString string) ([]OktaGroupRule, error) return oktaGroupRules, nil } -var reGroupRuleExpression = regexp.MustCompile(`"(\w{20})"`) +func regexMatcher(expression *regexp.Regexp, matchString string, regexGroupMatch bool) []string { + var regexMatches []string + matches := expression.FindAllStringSubmatch(matchString, -1) + + for _, match := range matches { + switch regexGroupMatch { + case false: + if match[0] != "" { + regexMatches = append(regexMatches, match[0]) + } + case true: + if len(match) < 2 { + continue + } + + if match[1] != "" { + regexMatches = append(regexMatches, match[1]) + } + } + } + return regexMatches +} + +// OR and AND. The AND doesn't work properly because the output is just a slice of strings which we infer as OR, not AND +var reDividers = regexp.MustCompile(`\|\||&&`) + +// this probably doesn't work properly with `isMemberOfGroupNameStartsWith` since it won't give the actual groups, just the prefix +var reGroupPrefixes = regexp.MustCompile(`^"?isMemberOf.*Group.*`) +var reGroupRuleExpression = regexp.MustCompile(`."(.+?)"`) // 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 { +func parseGroupRuleExpression(groupRules 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]) + divided := reDividers.Split(groupRules, -1) + for i := range divided { + trimmedString := strings.TrimSpace(divided[i]) + prefixParse := regexMatcher(reGroupPrefixes, trimmedString, false) + for _, s := range prefixParse { + ruleParse := regexMatcher(reGroupRuleExpression, s, true) + groupIDs = append(groupIDs, ruleParse...) } } - return groupIDs } diff --git a/client/client_test.go b/client/client_test.go index e38d76e..fe7f5d4 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -28,6 +28,29 @@ func TestParseGroupRuleExpression(t *testing.T) { "00gar7xacmKf3wNAt4x7", }, }, + { + name: "isMemberOfGroupName with OR", + input: `isMemberOfGroupName("20-my-team-name") || isMemberOfGroupName("20-my-other-team-name")`, + expected: []string{ + "20-my-team-name", + "20-my-other-team-name", + }, + }, + { + name: "isMemberOfGroupName with AND", + input: `isMemberOfGroupName("20-my-team-name") && isMemberOfGroupName("20-my-other-team-name")`, + expected: []string{ + "20-my-team-name", + "20-my-other-team-name", + }, + }, + { + name: `isMemberOfGroupNameStartsWith. Note this doesn't work properly for now because it won't give the actual groups, just the prefix`, + input: `isMemberOfGroupNameStartsWith("my-prefix-")`, + expected: []string{ + "my-prefix-", + }, + }, } for _, tc := range testCases { diff --git a/client/rules.go b/client/rules.go index 0abebdc..3248f6b 100644 --- a/client/rules.go +++ b/client/rules.go @@ -2,13 +2,23 @@ package client import ( "fmt" + "regexp" "slices" "strings" "sync" ) +type RuleType string + +const ( + RuleTypeGroup RuleType = "group" + RuleTypeName RuleType = "name" +) + +var reOktaGroupID = regexp.MustCompile(`^00g.{17}$`) + // PrintGroupRules prints all the group rules that have the searchGroup as either source or destination -func (oi *OIClient) PrintGroupRules(searchGroup string) error { +func (oi *OIClient) PrintGroupRules(searchString string, ruletype RuleType) error { var wg sync.WaitGroup var groups []OktaGroup @@ -30,7 +40,7 @@ func (oi *OIClient) PrintGroupRules(searchGroup string) error { wg.Add(1) go func() { var err error - rules, err = oi.ListGroupRules(searchGroup) + rules, err = oi.ListGroupRules(searchString) if err != nil { panic(err) } @@ -46,18 +56,32 @@ func (oi *OIClient) PrintGroupRules(searchGroup string) error { } // replace groupID with groupName in rules + // groupName can also be in plain text - first see if there is a match with groupID + // if not, treat groupName as plain text 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] + _, exists := groupIDMap[sourceGroupID] + if exists { + sourceGroupIDs[i] = groupIDMap[sourceGroupID] + } else { + // if sourceGroupID is not found and it matches the Okta group ID pattern, + // the group does not exist in Okta anymore. + match := reOktaGroupID.MatchString(sourceGroupID) + if match { + sourceGroupIDs[i] = sourceGroupID + " [missing in Okta!]" + } else { + sourceGroupIDs[i] = sourceGroupID + } + } } rule.SourceGroupIDs = sourceGroupIDs rules[i] = rule } - groupRulesString := filterRulesToFormatted(searchGroup, rules) + groupRulesString := filterRulesToFormatted(searchString, rules, ruletype) fmt.Print(groupRulesString) return nil @@ -65,9 +89,10 @@ func (oi *OIClient) PrintGroupRules(searchGroup string) error { // 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 { +func filterRulesToFormatted(searchString string, ogr []OktaGroupRule, ruletype RuleType) string { var filteredOgr []OktaGroupRule + nameMaxLength := 0 sourceMaxLength := 0 // Only pick the rules that have searchGroup as either source or destination @@ -75,38 +100,51 @@ func filterRulesToFormatted(searchGroup string, ogr []OktaGroupRule) string { // is this shouldAdd stuff even needed? shoulAdd := true - if strings.EqualFold(o.DestinationGroupID, searchGroup) { - shoulAdd = true - } + nameMaxLength = max(nameMaxLength, len(o.Name)) - var wantedSourceGroupValue string - for _, sourceGroupID := range o.SourceGroupIDs { - if strings.EqualFold(sourceGroupID, searchGroup) { - sourceMaxLength = max(sourceMaxLength, len(sourceGroupID)) + switch ruletype { + case RuleTypeGroup: + if strings.EqualFold(o.DestinationGroupID, searchString) { 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) { + var wantedSourceGroupValue string for _, sourceGroupID := range o.SourceGroupIDs { - sourceMaxLength = max(sourceMaxLength, len(sourceGroupID)) + if strings.EqualFold(sourceGroupID, searchString) { + sourceMaxLength = max(sourceMaxLength, len(sourceGroupID)) + shoulAdd = true + // we need separate value to make sure Capitalization is proper + wantedSourceGroupValue = 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}, + + if !shoulAdd { + continue + } + // Only add the dependency to/from wantedValue, ignore other rules + if strings.EqualFold(o.DestinationGroupID, searchString) { + 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, searchString) { + ogrNew := OktaGroupRule{ + Name: o.Name, + DestinationGroupID: o.DestinationGroupID, + SourceGroupIDs: []string{wantedSourceGroupValue}, + } + filteredOgr = append(filteredOgr, ogrNew) + } + case RuleTypeName: + if strings.EqualFold(o.Name, searchString) { + for _, sourceGroupID := range o.SourceGroupIDs { + sourceMaxLength = max(sourceMaxLength, len(sourceGroupID)) + } + filteredOgr = append(filteredOgr, o) } - filteredOgr = append(filteredOgr, ogrNew) } } @@ -115,7 +153,7 @@ func filterRulesToFormatted(searchGroup string, ogr []OktaGroupRule) string { for _, o := range filteredOgr { for _, sourceGroupID := range o.SourceGroupIDs { - printSlice = append(printSlice, fmt.Sprintf("%-*s -> %s", sourceMaxLength, sourceGroupID, o.DestinationGroupID)) + printSlice = append(printSlice, fmt.Sprintf("%-*s: %-*s -> %s", nameMaxLength, o.Name, sourceMaxLength, sourceGroupID, o.DestinationGroupID)) } } diff --git a/go.mod b/go.mod index 1564d2d..63cf9d2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/popsu/okta-info -go 1.21.1 +go 1.21.6 require ( github.com/okta/okta-sdk-golang/v2 v2.20.0 diff --git a/main.go b/main.go index 8bd3fe8..f09ef9e 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ func printHelp() { fmt.Println(" group - print users in a group") fmt.Println(" user - print groups for a user") fmt.Println(" diff - print users in any of groups 1 or 2 but not in groups 3 or 4") - fmt.Println(" rule - print group rules for a group") + fmt.Println(" rule [name/group] - print rules matching the search string or print group rules for a group") } func run() error { @@ -60,7 +60,13 @@ func run() error { return oic.PrintGroupDiff(groupsA, groupsB, hideDeprovisioned) case "rule": - return oic.PrintGroupRules(os.Args[2]) + switch os.Args[2] { + case "group", "name": + return oic.PrintGroupRules(os.Args[3], client.RuleType(os.Args[2])) + default: + printHelp() + os.Exit(1) + } default: printHelp() os.Exit(1)