Skip to content

Commit

Permalink
Group rules enhancements (#2)
Browse files Browse the repository at this point in the history
- Enhancements to group rule command
- Add subswitches to rule command: name & group
- Support searching by rule name
- Match also by plain-text group names
- Print group rule name
- Alert if a group ID is missing in Okta

Co-authored-by: Tommi Hovi <[email protected]>
  • Loading branch information
WORKINGJONNI and popsu authored Jan 16, 2024
1 parent c213d9c commit 570c35a
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 56 deletions.
16 changes: 9 additions & 7 deletions .github/workflows/goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ OKTA_INFO_API_TOKEN=<your-api-token>
okta-info diff <group-name-1> <group-name-2>
```

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 <group-name>
okta-info rule group <group-name> # Search using group name
okta-info rule name <rule name> # Search using rule name
```
49 changes: 37 additions & 12 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
23 changes: 23 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
100 changes: 69 additions & 31 deletions client/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -46,67 +56,95 @@ 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
}

// 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
for _, o := range ogr {
// 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)
}
}

Expand All @@ -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))
}
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 8 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func printHelp() {
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")
fmt.Println(" rule [name/group] <rule name/group name> - print rules matching the search string or print group rules for a group")
}

func run() error {
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 570c35a

Please sign in to comment.