Skip to content

Better data generation for fixture data#105

Open
jirwin wants to merge 3 commits into
mainfrom
jirwin/cko-2026
Open

Better data generation for fixture data#105
jirwin wants to merge 3 commits into
mainfrom
jirwin/cko-2026

Conversation

@jirwin
Copy link
Copy Markdown
Contributor

@jirwin jirwin commented Feb 25, 2026

Summary by CodeRabbit

  • New Features

    • Adds first-class App resources across the demo: app records, member and child‑group management, listing/detail APIs, and app event feed for sync.
    • Connector now surfaces apps and associates child groups with owning apps.
    • Expanded data generator to produce richer org/app-aware datasets and more realistic user profiles.
  • Chores

    • Default sample user count increased from 10 to 200.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 25, 2026

Walkthrough

Adds App as a first-class resource across client, connector, and demo data generation; extends the demo data generator with an organization hierarchy and multi-phase population flow; increases default demo user count from 10 to 200; and wires listing, paging, grant/revoke, event feeds, and group→app mappings for apps.

Changes

Cohort / File(s) Summary
Demo data generator & org model
pkg/client/data.go
Large addition: organization hierarchy (positions, departments, levels, regions, c-suite), SaaS/app definitions and mappings, multi-phase generator flow producing apps, app resources, groups, projects, roles, enriched users (attrs, manager linkage), and passwords. Adds App usage to dbResource.
Client persistence & API surface
pkg/client/client.go
Adds App type and persistence: insert/update app rows, rowToApp, ListApps, GetApp, ListAppsByUpdatedAt, GrantAppAccess, RevokeAppAccess.
Connector: resource builders & feeds
pkg/connector/apps.go, pkg/connector/events.go, pkg/connector/connector.go, pkg/connector/resource_types.go
Introduces appBuilder (resource syncer), appEventFeed (feed), registers app resource type and wires newAppBuilder/newAppEventFeed into Demo connector. Implements app listing/getting, entitlement surface, grants/revokes, and event pagination.
Connector: group → app mapping
pkg/connector/groups.go
Adds buildGroupToAppMapping and uses it in Group List/Get to set parent ResourceId for child groups owned by apps.
Config default
pkg/config/config.go
Increases default demo UserCountField from 10 to 200.

Sequence Diagram(s)

sequenceDiagram
    participant Generator as Demo Generator
    participant DB as Demo DB (client)
    participant Connector as Connector ResourceSyncer
    participant EventFeed as Connector EventFeed
    participant API as Client API

    Generator->>DB: build apps, groups, projects, roles, users, passwords
    DB-->>Generator: persisted Apps (id, members, child_groups, timestamps)
    Connector->>API: ListApps / ListAppsByUpdatedAt
    API-->>Connector: Apps (with Members, ChildGroups) + nextPageToken
    Connector->>Connector: build resources (App, Group mapping to App)
    Connector->>EventFeed: emit app ResourceChangeEvents (OccurredAt = UpdatedAt)
    EventFeed-->>Connector: streaming pages, Cursor/HasMore
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I stitched an org from data bright,
Apps and groups arranged just right,
Two hundred names in tidy rows,
Managers hop where soft wind blows,
The generator winks — the rabbit writes.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Better data generation for fixture data' is vague and generic, using non-descriptive terms that don't convey meaningful information about the actual changes (App resource support, enriched user attributes, organizational hierarchy, app-to-group mappings). Consider a more descriptive title like 'Add App resource support with organizational hierarchy and enriched user attributes' to better reflect the substantial scope and primary changes in this changeset.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jirwin/cko-2026

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/client/data.go (1)

200-223: Consider precomputing manager emails to avoid O(n²) user generation.

getManagerEmail scans all users, and it is called once per user in Phase 5. This is fine at 200 users, but it scales poorly for larger fixture sizes.

Also applies to: 423-425

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 200 - 223, getManagerEmail currently scans
all users for each user (O(n²)); instead compute manager emails once and look
them up: build a map keyed by department (and optionally separate for managers
vs non-managers) during user generation by iterating users once and using
getDepartment, isManagerTitle and getUserName to record the appropriate manager
email(s), then change getManagerEmail to simply return from that precomputed
map; update callers in Phase 5 to pass or access the precomputed mapping rather
than triggering repeated scans in getManagerEmail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/client/data.go`:
- Around line 245-250: The hash calculation casts signed ints to uint32
unsafely; add a bounds check for negative inputs (if userIdx < 0 || groupIdx < 0
return false or handle appropriately), then perform the hash using wider signed
types: cast userIdx and groupIdx to int64 and compute h as int64 (use int64
constants like 2654435761 and 2246822519), do the mixing using int64 arithmetic
(or convert to uint64 after the negative check to perform bitwise shifts
safely), and finally compute the positive modulus against 100 before comparing
with coveragePct; update the block referencing userIdx, groupIdx, h, and
coveragePct accordingly to eliminate the signed-to-unsigned conversion.

---

Nitpick comments:
In `@pkg/client/data.go`:
- Around line 200-223: getManagerEmail currently scans all users for each user
(O(n²)); instead compute manager emails once and look them up: build a map keyed
by department (and optionally separate for managers vs non-managers) during user
generation by iterating users once and using getDepartment, isManagerTitle and
getUserName to record the appropriate manager email(s), then change
getManagerEmail to simply return from that precomputed map; update callers in
Phase 5 to pass or access the precomputed mapping rather than triggering
repeated scans in getManagerEmail.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f801cca and 68ec1c5.

📒 Files selected for processing (2)
  • pkg/client/data.go
  • pkg/config/config.go

Comment thread pkg/client/data.go
Comment on lines +245 to +250
h := uint32(userIdx)*2654435761 + uint32(groupIdx)*2246822519
// Mix bits for better uniformity
h ^= h >> 16
h *= 0x45d9f3b
h ^= h >> 16
return int(h%100) < coveragePct
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and examine the shouldAssign function
fd -t f 'data.go' pkg/client/

# Read the specific lines around 245-250
sed -n '240,255p' pkg/client/data.go

Repository: ConductorOne/baton-demo

Length of output: 438


🏁 Script executed:

# Get the full shouldAssign function
rg -n -B10 'func shouldAssign' pkg/client/data.go | head -20

# Also verify the exact problem with a more complete view
sed -n '235,260p' pkg/client/data.go

Repository: ConductorOne/baton-demo

Length of output: 1331


🏁 Script executed:

# Check if there's a gosec configuration or CI configuration mentioning this
find . -name ".gosec*" -o -name "gosec.json" -o -name ".golangci.yml" -o -name "golangci.yml" 2>/dev/null | head -10

# Also search for G115 in the codebase or CI files
rg -i 'G115|gosec' --type yaml --type yml --type json -n . 2>/dev/null | head -20

# Check if there's a Makefile or script that runs gosec
fd -t f '(Makefile|\.mk|*\.sh)' | xargs rg -l 'gosec' 2>/dev/null || true

Repository: ConductorOne/baton-demo

Length of output: 2979


🏁 Script executed:

# Check the golangci.yml for gosec configuration
cat .golangci.yml | grep -A 20 'gosec'

# Let me also search for any G115 references in the codebase or issues
rg 'G115' . 2>/dev/null || echo "No G115 found"

# Let's verify what G115 is - search for "implicit" or "conversion" context around the issue
rg -i 'implicit.*conversion|narrowing' pkg/client/data.go || true

Repository: ConductorOne/baton-demo

Length of output: 438


🏁 Script executed:

# Let's search for the actual gosec rule to understand what issue we're dealing with
# G115 in gosec is typically about implicit type conversions
# Let's verify the exact line numbers and the cast operation

sed -n '245p' pkg/client/data.go

# Let's also check if there are any build/CI logs mentioning this issue
find . -name "*.md" -o -name "*.txt" | xargs rg -l 'G115\|narrowing\|uint32' 2>/dev/null | grep -v vendor | head -5

Repository: ConductorOne/baton-demo

Length of output: 129


🏁 Script executed:

# Let's check gosec documentation - search for what rules gosec has
# and understand if G115 is about explicit or implicit conversions

# First, let's see if there's any CI configuration that runs gosec
find . -name "*.yml" -o -name "*.yaml" | grep -v vendor | xargs rg -l 'gosec' 2>/dev/null || true

# Check for GitHub workflows or CI files
fd -t f '(workflow|\.github)' 2>/dev/null | head -5

# Let's also check if this is a real issue by looking for gosec comments in the code
rg -i '#nosec|nolint.*gosec|G115' pkg/client/ 2>/dev/null || echo "No gosec directives found"

Repository: ConductorOne/baton-demo

Length of output: 574


Fix G115 at line 245: Remove unsafe signed-to-unsigned int conversion in hash calculation.

The current code casts signed int parameters directly to uint32, which can produce undefined behavior when negative values are passed. This narrowing conversion is flagged by gosec and blocks CI.

Change the hash calculation to use int64 instead of uint32 to avoid signed-to-unsigned conversion issues, and add bounds checks for negative input values:

Proposed fix
+	if userIdx < 0 || groupIdx < 0 {
+		return false
+	}
 	// Knuth multiplicative hash for good distribution
-	h := uint32(userIdx)*2654435761 + uint32(groupIdx)*2246822519
+	h := int64(userIdx)*2654435761 + int64(groupIdx)*2246822519
 	// Mix bits for better uniformity
 	h ^= h >> 16
 	h *= 0x45d9f3b
 	h ^= h >> 16
+	if h < 0 {
+		h = -h
+	}
 	return int(h%100) < coveragePct
🧰 Tools
🪛 GitHub Actions: ci

[error] 245-245: G115: integer overflow conversion int -> uint32 (gosec)

🪛 GitHub Check: go-lint

[failure] 245-245:
G115: integer overflow conversion int -> uint32 (gosec)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 245 - 250, The hash calculation casts signed
ints to uint32 unsafely; add a bounds check for negative inputs (if userIdx < 0
|| groupIdx < 0 return false or handle appropriately), then perform the hash
using wider signed types: cast userIdx and groupIdx to int64 and compute h as
int64 (use int64 constants like 2654435761 and 2246822519), do the mixing using
int64 arithmetic (or convert to uint64 after the negative check to perform
bitwise shifts safely), and finally compute the positive modulus against 100
before comparing with coveragePct; update the block referencing userIdx,
groupIdx, h, and coveragePct accordingly to eliminate the signed-to-unsigned
conversion.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/client/data.go (1)

987-1001: ⚠️ Potential issue | 🟠 Major

Nil *dbResource returned alongside true in Phase 4 causes panic when Projects or Roles config is 0

When g.config.Projects == 0 or g.config.Roles == 0, the ScopedRole phase at lines 987-1001 returns (nil, true). The caller in pkg/client/client.go:230 checks only the boolean flag and immediately enters a switch statement that accesses fields on the nil pointer (e.g., dbResource.User, dbResource.ScopedRole), causing a panic. This occurs whenever ScopedRoles > 0 but the Projects or Roles config is zero.

Add a precondition check to skip the phase or return from Next() when the required config is not satisfied, avoiding the nil return.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 987 - 1001, The ScopedRole branch in Next()
can return (nil, true) when g.config.Projects==0 or g.config.Roles==0 which
causes panics; add a precondition in the ScopedRole generation (around
dbResource/ScopedRole creation using g.currentScopedRole and g.config) to detect
if g.config.Projects==0 or g.config.Roles==0 and in that case skip the phase (do
not return a nil dbResource with true) — either increment g.currentScopedRole
and continue to the next iteration or return (nil, false) so callers of Next()
won’t treat a nil *dbResource as present; update the logic around dbResource,
ScopedRole, g.currentScopedRole and the boolean return to ensure a non-nil
*dbResource is only returned with true.
♻️ Duplicate comments (1)
pkg/client/data.go (1)

778-782: ⚠️ Potential issue | 🔴 Critical

G115: int → uint32 conversion in shouldAssign still unresolved (CI blocked).

This is the same issue flagged in the previous review round. The uint32(userIdx) and uint32(groupIdx) casts are unsafe for negative inputs and are blocking CI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 778 - 782, The unsafe casts in shouldAssign
occur because userIdx and groupIdx may be negative before uint32(...)
conversion; add an explicit non-negative check at the start of shouldAssign
(e.g., if userIdx < 0 || groupIdx < 0 { return false } or handle negatives
according to desired semantics) and only then cast to uint32 for the hashing
math; reference the shouldAssign function and the userIdx, groupIdx, and
coveragePct variables when applying this guard so CI no longer flags the
int→uint32 conversion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/client/data.go`:
- Line 502: Extract the repeated role strings into named constants (e.g., const
Contractor = "Contractor", ServiceAccount = "Service Account", SharedAccount =
"Shared Account") and replace every literal occurrence (for example the
conditional checking level in the if statement that uses "IC", "Senior IC", and
"Contractor") with the constants (use Contractor, ServiceAccount, SharedAccount)
to satisfy goconst; add the new const block near other package-level constants
and update all references throughout the file where those string literals
appear.
- Around line 942-944: The GroupAssignments slice is being populated with
possibly-duplicate entries because groupId(g.currentProject % totalGroups) can
equal groupId((g.currentProject * 10) % totalGroups); update the assignment
logic that builds GroupAssignments (referencing g.currentProject, totalGroups
and groupId) to deduplicate values before writing them (e.g., collect the two
computed ids into a small set or map, then convert back to a slice) so only
unique groupId values are stored and downstream uniqueness constraints are not
violated.
- Around line 704-706: In getRegionForUser, avoid the unsafe int→uint32
conversion for userIdx: ensure userIdx is non-negative or cast via uint before
converting to uint32 (e.g., validate userIdx >= 0 and then use
uint32(uint(userIdx))) so the expression that assigns h (currently: h :=
uint32(userIdx) * 2654435761) is safe; alternatively, add an explicit guard that
normalizes or rejects negative userIdx values before computing h and pct to
satisfy the G115 check.
- Around line 828-898: Service/shared accounts currently fall through into the
full member/admin assignment path; modify the logic so that when
pos.EmploymentType == "Service Account" || "Shared Account" and they pass the
initial non-universal dept check they do not continue through the standard
assignment/admin-evaluation block. Concretely: either wrap the large block
starting at "dept := pos.Department" through admin appends in an else branch of
the initial service/shared if, or after the service/shared check set a
short-circuit (e.g., append as member and continue) so service/shared accounts
never reach the isAdmin calculation (shouldAssign(...,2), isOverPrivileged, VP
check); reference g.positions / pos.EmploymentType, isUniversalApp,
shouldAssign, dept := pos.Department, isOverPrivileged, admins and members to
locate the code to change.
- Around line 1010-1018: The switch on pos.EmploymentType can be empty for tail
positions when buildOrgChart computed effectiveSpecial = 0; add a defensive
guard before that switch in the function that builds the user row (check
pos.EmploymentType == "" or strings.TrimSpace(pos.EmploymentType) == "") and
handle it explicitly (e.g., set fullName and email to empty strings or skip
calling getUserName) so the default branch does not pull incorrect names; keep
existing cases for "Service Account" and "Shared Account" and reference
serviceAccountDefs[g.currentUser-(g.config.Users-numSpecialAccounts)] and
sharedAccountDefs[g.currentUser-(g.config.Users-numSharedAccounts)] unchanged
for their cases.

---

Outside diff comments:
In `@pkg/client/data.go`:
- Around line 987-1001: The ScopedRole branch in Next() can return (nil, true)
when g.config.Projects==0 or g.config.Roles==0 which causes panics; add a
precondition in the ScopedRole generation (around dbResource/ScopedRole creation
using g.currentScopedRole and g.config) to detect if g.config.Projects==0 or
g.config.Roles==0 and in that case skip the phase (do not return a nil
dbResource with true) — either increment g.currentScopedRole and continue to the
next iteration or return (nil, false) so callers of Next() won’t treat a nil
*dbResource as present; update the logic around dbResource, ScopedRole,
g.currentScopedRole and the boolean return to ensure a non-nil *dbResource is
only returned with true.

---

Duplicate comments:
In `@pkg/client/data.go`:
- Around line 778-782: The unsafe casts in shouldAssign occur because userIdx
and groupIdx may be negative before uint32(...) conversion; add an explicit
non-negative check at the start of shouldAssign (e.g., if userIdx < 0 ||
groupIdx < 0 { return false } or handle negatives according to desired
semantics) and only then cast to uint32 for the hashing math; reference the
shouldAssign function and the userIdx, groupIdx, and coveragePct variables when
applying this guard so CI no longer flags the int→uint32 conversion.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68ec1c5 and 9861011.

📒 Files selected for processing (1)
  • pkg/client/data.go

Comment thread pkg/client/data.go
if totalUsers < 50 || userIdx < numCxO {
return false
}
if level != "IC" && level != "Senior IC" && level != "Contractor" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Pipeline failure: extract "Contractor" (and "Service Account", "Shared Account") as named constants to fix the CI goconst error.

golangci-lint reports that the string literal "Contractor" appears 3+ times and the pipeline is blocked. The same lint rule applies to "Service Account" and "Shared Account", which have equally many occurrences.

🔧 Proposed fix
+const (
+	empTypeFullTime       = "Full-time"
+	empTypeContractor     = "Contractor"
+	empTypeServiceAccount = "Service Account"
+	empTypeSharedAccount  = "Shared Account"
+)

Then replace every matching string literal throughout the file (e.g., line 502, 636, 660, 831, 1010–1015, etc.) with the corresponding constant.

Also applies to: 636-637, 660-660

🧰 Tools
🪛 GitHub Actions: ci

[error] 502-502: golangci-lint error: string Contractor has 3 occurrences, make it a constant (goconst).

🪛 GitHub Check: go-lint

[failure] 502-502:
string Contractor has 3 occurrences, make it a constant (goconst)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` at line 502, Extract the repeated role strings into named
constants (e.g., const Contractor = "Contractor", ServiceAccount = "Service
Account", SharedAccount = "Shared Account") and replace every literal occurrence
(for example the conditional checking level in the if statement that uses "IC",
"Senior IC", and "Contractor") with the constants (use Contractor,
ServiceAccount, SharedAccount) to satisfy goconst; add the new const block near
other package-level constants and update all references throughout the file
where those string literals appear.

Comment thread pkg/client/data.go
Comment on lines +704 to +706
h := uint32(userIdx) * 2654435761
h ^= h >> 16
pct := int(h % 100)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix G115: unsafe int → uint32 conversion in getRegionForUser.

Same gosec rule (G115) that blocks CI fires here. userIdx is always non-negative at its call sites, but the compiler does not guarantee that; add an explicit guard or cast through uint.

🐛 Proposed fix
 func getRegionForUser(userIdx int) string {
-	h := uint32(userIdx) * 2654435761
+	if userIdx < 0 {
+		userIdx = -userIdx
+	}
+	h := uint32(userIdx) * 2654435761 // `#nosec` G115 -- non-negative guard above
 	h ^= h >> 16
 	pct := int(h % 100)

Alternatively, annotate with //nolint:gosec (or #nosec G115) if the project's lint config accepts that style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
h := uint32(userIdx) * 2654435761
h ^= h >> 16
pct := int(h % 100)
if userIdx < 0 {
userIdx = -userIdx
}
h := uint32(userIdx) * 2654435761 // `#nosec` G115 -- non-negative guard above
h ^= h >> 16
pct := int(h % 100)
🧰 Tools
🪛 GitHub Check: go-lint

[failure] 704-704:
G115: integer overflow conversion int -> uint32 (gosec)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 704 - 706, In getRegionForUser, avoid the
unsafe int→uint32 conversion for userIdx: ensure userIdx is non-negative or cast
via uint before converting to uint32 (e.g., validate userIdx >= 0 and then use
uint32(uint(userIdx))) so the expression that assigns h (currently: h :=
uint32(userIdx) * 2654435761) is safe; alternatively, add an explicit guard that
normalizes or rejects negative userIdx values before computing h and pct to
satisfy the G115 check.

Comment thread pkg/client/data.go Outdated
Comment on lines +828 to +898
for i := 0; i < g.config.Users; i++ {
pos := g.positions[i]

if pos.EmploymentType == "Service Account" || pos.EmploymentType == "Shared Account" {
if !g.isUniversalApp(g.currentGroup) {
coverage, inDept := app.DeptCoverage[pos.Department]
if !inDept || !shouldAssign(i, g.currentGroup, coverage) {
continue
}
}
}

dept := pos.Department
coverage, inTargetDept := app.DeptCoverage[dept]

assign := false
if inTargetDept {
assign = shouldAssign(i, g.currentGroup, coverage)
} else if app.NoisePct > 0 {
assign = shouldAssign(i, g.currentGroup+1000, app.NoisePct)
}

// Level gate
if app.MinLevel != "" && assign {
userLvl := levelOrder[pos.Level]
minLvl := levelOrder[app.MinLevel]
if userLvl < minLvl {
// Acting managers bypass the manager-level gate
if app.MinLevel == "Manager" && isActingManager(i, g.config.Users, pos.Level) {
// allowed
} else {
assign = false
}
}
}

// Region gate
if app.RegionOnly != "" && assign {
if pos.Region != app.RegionOnly {
assign = false
}
}

// Edge: transferred users retain old department access
if !assign && isTransferred(i, g.config.Users, pos.Department) {
oldDept := getTransferredFromDept(i, pos.Department)
if oldCov, ok := app.DeptCoverage[oldDept]; ok {
assign = shouldAssign(i, g.currentGroup, oldCov)
}
}

// Edge: over-privileged ICs gain random extra access
if !assign && isOverPrivileged(i, g.config.Users, pos.Level) {
assign = shouldAssign(i, g.currentGroup+3000, 30)
}

if assign {
members = append(members, userId(i))

isAdmin := shouldAssign(i, g.currentGroup, 2)
if isOverPrivileged(i, g.config.Users, pos.Level) {
isAdmin = shouldAssign(i, g.currentGroup, 25)
}
if inTargetDept && levelOrder[pos.Level] >= levelOrder["VP"] {
isAdmin = true
}
if isAdmin {
admins = append(admins, userId(i))
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Service/Shared accounts fall through to the full member-assignment path and can be elected as group admins.

When a service/shared account passes the early-gate check (lines 831-838) in a non-universal app, execution falls through to the standard path. Because there is no else or early continue after the service-account block, those accounts run through every downstream check including the 2% admin election (shouldAssign(i, g.currentGroup, 2), line 887) and the isOverPrivileged admin path (line 888-890). Service and shared accounts becoming group admins is almost certainly unintended.

♻️ Proposed fix — add an explicit `else` block for regular users
 			if pos.EmploymentType == "Service Account" || pos.EmploymentType == "Shared Account" {
 				if !g.isUniversalApp(g.currentGroup) {
 					coverage, inDept := app.DeptCoverage[pos.Department]
 					if !inDept || !shouldAssign(i, g.currentGroup, coverage) {
 						continue
 					}
 				}
+				// Service/shared accounts that reach here become plain members only.
+				members = append(members, userId(i))
+				continue
 			}
 
 			dept := pos.Department
🧰 Tools
🪛 GitHub Check: go-lint

[failure] 831-831:
string Service Account has 3 occurrences, make it a constant (goconst)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 828 - 898, Service/shared accounts currently
fall through into the full member/admin assignment path; modify the logic so
that when pos.EmploymentType == "Service Account" || "Shared Account" and they
pass the initial non-universal dept check they do not continue through the
standard assignment/admin-evaluation block. Concretely: either wrap the large
block starting at "dept := pos.Department" through admin appends in an else
branch of the initial service/shared if, or after the service/shared check set a
short-circuit (e.g., append as member and continue) so service/shared accounts
never reach the isAdmin calculation (shouldAssign(...,2), isOverPrivileged, VP
check); reference g.positions / pos.EmploymentType, isUniversalApp,
shouldAssign, dept := pos.Department, isOverPrivileged, admins and members to
locate the code to change.

Comment thread pkg/client/data.go Outdated
Comment on lines 942 to 944
groupId(g.currentProject % totalGroups),
groupId((g.currentProject * 10) % totalGroups),
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Project group assignments may contain duplicates.

When g.currentProject % totalGroups == (g.currentProject * 10) % totalGroups (e.g., every 10th project with the current ~90 appGroups), the same groupId is written twice into GroupAssignments. Downstream uniqueness constraints (e.g., the DB schema) may reject these rows.

♻️ Proposed dedup
-			GroupAssignments: []string{
-				groupId(g.currentProject % totalGroups),
-				groupId((g.currentProject * 10) % totalGroups),
-			},
+			GroupAssignments: func() []string {
+				a := groupId(g.currentProject % totalGroups)
+				b := groupId((g.currentProject * 10) % totalGroups)
+				if a == b {
+					return []string{a}
+				}
+				return []string{a, b}
+			}(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
groupId(g.currentProject % totalGroups),
groupId((g.currentProject * 10) % totalGroups),
},
GroupAssignments: func() []string {
a := groupId(g.currentProject % totalGroups)
b := groupId((g.currentProject * 10) % totalGroups)
if a == b {
return []string{a}
}
return []string{a, b}
}(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 942 - 944, The GroupAssignments slice is
being populated with possibly-duplicate entries because groupId(g.currentProject
% totalGroups) can equal groupId((g.currentProject * 10) % totalGroups); update
the assignment logic that builds GroupAssignments (referencing g.currentProject,
totalGroups and groupId) to deduplicate values before writing them (e.g.,
collect the two computed ids into a small set or map, then convert back to a
slice) so only unique groupId values are stored and downstream uniqueness
constraints are not violated.

Comment thread pkg/client/data.go
Comment on lines +1010 to +1018
switch pos.EmploymentType {
case "Service Account":
sa := serviceAccountDefs[g.currentUser-(g.config.Users-numSpecialAccounts)]
fullName = sa.Name
email = sa.Email
case "Shared Account":
sa := sharedAccountDefs[g.currentUser-(g.config.Users-numSharedAccounts)]
fullName = sa.Name
email = sa.Email
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and read the relevant code sections
head -100 pkg/client/data.go | tail -20

Repository: ConductorOne/baton-demo

Length of output: 1869


🏁 Script executed:

# Get the total size of the file first
wc -l pkg/client/data.go

Repository: ConductorOne/baton-demo

Length of output: 90


🏁 Script executed:

# Read lines around 1010-1018 to see the exact code
sed -n '1005,1025p' pkg/client/data.go

Repository: ConductorOne/baton-demo

Length of output: 735


🏁 Script executed:

# Find buildOrgChart function to verify the position layout claims
rg -n "func.*buildOrgChart" pkg/client/data.go -A 2

Repository: ConductorOne/baton-demo

Length of output: 176


🏁 Script executed:

# Find where effectiveSpecial is set
rg -n "effectiveSpecial\s*=" pkg/client/data.go -B 2 -A 2

Repository: ConductorOne/baton-demo

Length of output: 199


🏁 Script executed:

# Find numSpecialAccounts and numSharedAccounts definitions
rg -n "numSpecialAccounts|numSharedAccounts" pkg/client/data.go -B 1 -A 1

Repository: ConductorOne/baton-demo

Length of output: 853


🏁 Script executed:

# Find EmploymentType related constants
rg -n "empType|EmploymentType" pkg/client/data.go | head -30

Repository: ConductorOne/baton-demo

Length of output: 576


🏁 Script executed:

# Find numCxO constant
rg -n "const numCxO" pkg/client/data.go

Repository: ConductorOne/baton-demo

Length of output: 86


🏁 Script executed:

# Read the full buildOrgChart function to verify position layout
sed -n '558,680p' pkg/client/data.go

Repository: ConductorOne/baton-demo

Length of output: 3001


🏁 Script executed:

# Check if there are other places using these special account indices
rg -n "serviceAccountDefs\[|sharedAccountDefs\[" pkg/client/data.go

Repository: ConductorOne/baton-demo

Length of output: 299


🏁 Script executed:

# Verify the default config.Users value
rg -n "config\.Users|DefaultUsers" pkg/client/data.go | head -20

Repository: ConductorOne/baton-demo

Length of output: 1304


🏁 Script executed:

# Check if there are other uses of pos.EmploymentType in the file
rg -n "pos\.EmploymentType|\.EmploymentType" pkg/client/data.go -B 1 -A 1

Repository: ConductorOne/baton-demo

Length of output: 535


🏁 Script executed:

# Verify the suggested variable empTypeFullTime exists
rg -n "empTypeFullTime|\"Full-time\"" pkg/client/data.go | head -10

Repository: ConductorOne/baton-demo

Length of output: 131


Add defensive guard when EmploymentType is empty to handle the edge case where totalUsers ≤ 13.

When config.Users ≤ 13 (numCxO + numSpecialAccounts), buildOrgChart sets effectiveSpecial = 0 and the service/shared account population loops never execute, leaving those tail positions with zero-value EmploymentType strings. The switch statement will then fall to the default case for those indices, producing incorrect names/emails from getUserName. Although this edge case is unreachable with the default 200 users, the guard below closes the hole:

🛡️ Suggested defensive guard
 	if g.currentUser < g.config.Users {
 		pos := g.positions[g.currentUser]
 		first, last := getUserName(g.currentUser)
+		// Sanity-check: if effectiveSpecial was 0, EmploymentType is empty;
+		// treat as Full-time rather than mis-indexing a special-account slice.
+		empType := pos.EmploymentType
+		if empType == "" {
+			empType = "Full-time"
+		}
 		var fullName, email string
-		switch pos.EmploymentType {
+		switch empType {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 1010 - 1018, The switch on
pos.EmploymentType can be empty for tail positions when buildOrgChart computed
effectiveSpecial = 0; add a defensive guard before that switch in the function
that builds the user row (check pos.EmploymentType == "" or
strings.TrimSpace(pos.EmploymentType) == "") and handle it explicitly (e.g., set
fullName and email to empty strings or skip calling getUserName) so the default
branch does not pull incorrect names; keep existing cases for "Service Account"
and "Shared Account" and reference
serviceAccountDefs[g.currentUser-(g.config.Users-numSpecialAccounts)] and
sharedAccountDefs[g.currentUser-(g.config.Users-numSharedAccounts)] unchanged
for their cases.

…ling

- Added App struct to represent application entities.
- Implemented methods for listing, retrieving, granting, and revoking access to apps in the client.
- Introduced app event feed for tracking changes to app resources.
- Enhanced group handling to associate child groups with their parent apps.
- Updated resource types to include app resources for better integration with the connector.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
pkg/client/data.go (3)

1067-1070: ⚠️ Potential issue | 🟡 Minor

Deduplicate project GroupAssignments to avoid repeated IDs.

Line 1067 can emit the same group twice when both computed indices collide.

🔧 Proposed fix
 		if totalValid > 0 {
-			groupAssignments = []string{
-				groupId(validGroups[g.currentProject%totalValid]),
-				groupId(validGroups[(g.currentProject*10)%totalValid]),
-			}
+			a := groupId(validGroups[g.currentProject%totalValid])
+			b := groupId(validGroups[(g.currentProject*10)%totalValid])
+			if a == b {
+				groupAssignments = []string{a}
+			} else {
+				groupAssignments = []string{a, b}
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 1067 - 1070, The groupAssignments creation
can produce duplicate IDs when the two computed indices collide; update the
logic around groupAssignments (the slice built from groupId(validGroups[...])
using g.currentProject and totalValid) to ensure unique entries by checking the
second computed groupId against the first and only appending if different (or
use a small dedup set before assigning), e.g., compute both candidate indices
from g.currentProject and (g.currentProject*10), map them through
groupId(validGroups[...]) and build groupAssignments with only distinct IDs so
no repeated group appears.

914-921: ⚠️ Potential issue | 🟠 Major

Service/shared accounts can still be elevated to group admins.

After the check at Line 914, execution falls through to the normal admin election path at Line 969. Service/shared accounts should short-circuit as member-only.

🔧 Proposed fix
 		if pos.EmploymentType == "Service Account" || pos.EmploymentType == "Shared Account" {
 			if !g.isUniversalApp(groupIdx) {
 				coverage, inDept := app.DeptCoverage[pos.Department]
 				if !inDept || !shouldAssign(i, groupIdx, coverage) {
 					continue
 				}
 			}
+			members = append(members, userId(i))
+			continue
 		}

Also applies to: 969-978

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 914 - 921, Service/shared accounts
(pos.EmploymentType == "Service Account" || "Shared Account") are still falling
through into the admin-election logic; ensure they are treated as member-only by
short-circuiting them before the admin-election path. In the block that checks
pos.EmploymentType (and uses g.isUniversalApp, app.DeptCoverage, shouldAssign),
either immediately set the role to member and continue the loop or add an
explicit continue to skip any subsequent admin-election code (the code that runs
the admin election logic for the group) so service/shared accounts never reach
the admin election logic.

767-770: ⚠️ Potential issue | 🔴 Critical

Fix unsafe int -> uint32 casts in hash helpers (G115).

Line 768 and Line 842 still cast signed indices directly to uint32. This matches the previous lint-blocking G115 finding.

🔧 Proposed fix
 func getRegionForUser(userIdx int) string {
-	h := uint32(userIdx) * 2654435761
+	if userIdx < 0 {
+		return "Americas"
+	}
+	h := int64(userIdx) * 2654435761
 	h ^= h >> 16
+	if h < 0 {
+		h = -h
+	}
 	pct := int(h % 100)
 	for _, r := range regionDistribution {
 		if pct < r.Weight {
 			return r.Name
 		}
 	}
 	return "Americas"
 }
@@
 func shouldAssign(userIdx, groupIdx, coveragePct int) bool {
 	if coveragePct >= 100 {
 		return true
 	}
 	if coveragePct <= 0 {
 		return false
 	}
-	h := uint32(userIdx)*2654435761 + uint32(groupIdx)*2246822519
+	if userIdx < 0 || groupIdx < 0 {
+		return false
+	}
+	h := int64(userIdx)*2654435761 + int64(groupIdx)*2246822519
 	h ^= h >> 16
 	h *= 0x45d9f3b
 	h ^= h >> 16
+	if h < 0 {
+		h = -h
+	}
 	return int(h%100) < coveragePct
 }
#!/bin/bash
# Verify current signed->unsigned casts in hash helpers (G115 candidates).
# Expected before fix: matches at getRegionForUser and shouldAssign.
# Expected after fix: no direct uint32(userIdx/groupIdx) casts.

rg -nP 'uint32\(\s*(userIdx|groupIdx)\s*\)' pkg/client/data.go
sed -n '764,772p' pkg/client/data.go
sed -n '835,847p' pkg/client/data.go

Also applies to: 842-846

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/data.go` around lines 767 - 770, Replace the unsafe direct
signed->unsigned casts by first narrowing to a 32-bit signed type then to
uint32: locate getRegionForUser and shouldAssign in pkg/client/data.go and
change occurrences of uint32(userIdx) and uint32(groupIdx) to use
uint32(int32(userIdx)) and uint32(int32(groupIdx)) respectively so the
conversion is explicit and avoids the G115 unsafe cast; keep the rest of the
hash computation (h ^= h >> 16, pct := int(h % 100), etc.) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/connector/apps.go`:
- Around line 159-170: The code calls parseAppID(entitlement.Id) but ignores the
parsed entitlement variant/slug and proceeds to GrantAppAccess/RevokeAppAccess;
update both grant and revoke paths to check the second return value from
parseAppID (e.g., entType or slug) and verify it equals the expected entitlement
variant (reject otherwise), and if it doesn't match return a clear error instead
of proceeding; locate parseAppID usage and the calls to o.client.GrantAppAccess
and o.client.RevokeAppAccess and add the validation and explicit error return in
those places.

In `@pkg/connector/groups.go`:
- Around line 371-377: The buildGroupToAppMapping function currently swallows
errors from o.client.ListApps and returns an empty map; change
buildGroupToAppMapping to return (map[string]*v2.ResourceId, error) instead of
only map so callers can handle failures, update the implementation to return the
error from o.client.ListApps when it occurs (instead of an empty map), and
adjust all callers of buildGroupToAppMapping to handle the returned error
(propagate or handle appropriately) so parent app links are not silently
dropped; references: function buildGroupToAppMapping and the call to
o.client.ListApps in pkg/connector/groups.go.

---

Duplicate comments:
In `@pkg/client/data.go`:
- Around line 1067-1070: The groupAssignments creation can produce duplicate IDs
when the two computed indices collide; update the logic around groupAssignments
(the slice built from groupId(validGroups[...]) using g.currentProject and
totalValid) to ensure unique entries by checking the second computed groupId
against the first and only appending if different (or use a small dedup set
before assigning), e.g., compute both candidate indices from g.currentProject
and (g.currentProject*10), map them through groupId(validGroups[...]) and build
groupAssignments with only distinct IDs so no repeated group appears.
- Around line 914-921: Service/shared accounts (pos.EmploymentType == "Service
Account" || "Shared Account") are still falling through into the admin-election
logic; ensure they are treated as member-only by short-circuiting them before
the admin-election path. In the block that checks pos.EmploymentType (and uses
g.isUniversalApp, app.DeptCoverage, shouldAssign), either immediately set the
role to member and continue the loop or add an explicit continue to skip any
subsequent admin-election code (the code that runs the admin election logic for
the group) so service/shared accounts never reach the admin election logic.
- Around line 767-770: Replace the unsafe direct signed->unsigned casts by first
narrowing to a 32-bit signed type then to uint32: locate getRegionForUser and
shouldAssign in pkg/client/data.go and change occurrences of uint32(userIdx) and
uint32(groupIdx) to use uint32(int32(userIdx)) and uint32(int32(groupIdx))
respectively so the conversion is explicit and avoids the G115 unsafe cast; keep
the rest of the hash computation (h ^= h >> 16, pct := int(h % 100), etc.)
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b726e76e-c1aa-4b72-8d61-056c6b0e7318

📥 Commits

Reviewing files that changed from the base of the PR and between 9861011 and c10a820.

📒 Files selected for processing (7)
  • pkg/client/client.go
  • pkg/client/data.go
  • pkg/connector/apps.go
  • pkg/connector/connector.go
  • pkg/connector/events.go
  • pkg/connector/groups.go
  • pkg/connector/resource_types.go

Comment thread pkg/connector/apps.go
Comment on lines +159 to +170
appID, _, err := parseAppID(entitlement.Id)
if err != nil {
return nil, nil, err
}
userID := principal.Id.Resource

err = o.client.GrantAppAccess(ctx, appID, userID)
if err != nil {
return nil, nil, err
}

return nil, nil, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate entitlement slug before app grant/revoke mutations.

Line 159 ignores the parsed entitlement type, and Line 173 revokes without checking slug. Unknown entitlement variants should be rejected explicitly.

🔧 Proposed fix
-	appID, _, err := parseAppID(entitlement.Id)
+	appID, grantType, err := parseAppID(entitlement.Id)
 	if err != nil {
 		return nil, nil, err
 	}
+	if grantType != appAccessEntitlement {
+		return nil, nil, status.Errorf(codes.InvalidArgument, "unknown entitlement %q", grantType)
+	}
@@
 func (o *appBuilder) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) {
 	appID := grant.Entitlement.Resource.Id.Resource
 	principalId := grant.Principal.Id
@@
+	if grant.Entitlement.Slug != appAccessEntitlement {
+		return nil, status.Errorf(codes.InvalidArgument, "unknown entitlement")
+	}
+
 	err := o.client.RevokeAppAccess(ctx, appID, principalId.Resource)

Also applies to: 173-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/apps.go` around lines 159 - 170, The code calls
parseAppID(entitlement.Id) but ignores the parsed entitlement variant/slug and
proceeds to GrantAppAccess/RevokeAppAccess; update both grant and revoke paths
to check the second return value from parseAppID (e.g., entType or slug) and
verify it equals the expected entitlement variant (reject otherwise), and if it
doesn't match return a clear error instead of proceeding; locate parseAppID
usage and the calls to o.client.GrantAppAccess and o.client.RevokeAppAccess and
add the validation and explicit error return in those places.

Comment thread pkg/connector/groups.go
Comment on lines +371 to +377
// buildGroupToAppMapping builds a lookup from child group IDs to their parent app resource IDs.
func (o *groupBuilder) buildGroupToAppMapping(ctx context.Context) map[string]*v2.ResourceId {
result := make(map[string]*v2.ResourceId)
apps, err := o.client.ListApps(ctx)
if err != nil {
return result
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not swallow ListApps failures in group→app mapping.

Line 375 currently hides failures and returns an empty mapping, which silently drops parent links in List/Get.

🔧 Proposed fix
-	groupToApp := o.buildGroupToAppMapping(ctx)
+	groupToApp, err := o.buildGroupToAppMapping(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
@@
-	groupToApp := o.buildGroupToAppMapping(ctx)
+	groupToApp, err := o.buildGroupToAppMapping(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
@@
-func (o *groupBuilder) buildGroupToAppMapping(ctx context.Context) map[string]*v2.ResourceId {
+func (o *groupBuilder) buildGroupToAppMapping(ctx context.Context) (map[string]*v2.ResourceId, error) {
 	result := make(map[string]*v2.ResourceId)
 	apps, err := o.client.ListApps(ctx)
 	if err != nil {
-		return result
+		return nil, err
 	}
@@
-	return result
+	return result, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/groups.go` around lines 371 - 377, The buildGroupToAppMapping
function currently swallows errors from o.client.ListApps and returns an empty
map; change buildGroupToAppMapping to return (map[string]*v2.ResourceId, error)
instead of only map so callers can handle failures, update the implementation to
return the error from o.client.ListApps when it occurs (instead of an empty
map), and adjust all callers of buildGroupToAppMapping to handle the returned
error (propagate or handle appropriately) so parent app links are not silently
dropped; references: function buildGroupToAppMapping and the call to
o.client.ListApps in pkg/connector/groups.go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants